mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-02-22 08:52:06 +00:00
Compare commits
30 Commits
feat/ldap-
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff771c5c22 | ||
|
|
d7b00ffeea | ||
|
|
22c4c262ea | ||
|
|
baf4798665 | ||
|
|
bea680edec | ||
|
|
3eea68ae0c | ||
|
|
f08d8593ea | ||
|
|
fa1c5292f9 | ||
|
|
ce25f9561f | ||
|
|
f24595b24e | ||
|
|
285edba88c | ||
|
|
51d95fa455 | ||
|
|
fd16f91011 | ||
|
|
fb671139cd | ||
|
|
7ca79d4532 | ||
|
|
9d2d08a537 | ||
|
|
15ee55ca61 | ||
|
|
eff5fc8b71 | ||
|
|
671343f677 | ||
|
|
252ba10f48 | ||
|
|
6431afb7d1 | ||
|
|
f9b221778f | ||
|
|
7ed6174140 | ||
|
|
bbf31be5ae | ||
|
|
402dfa727b | ||
|
|
d67c3ab8a4 | ||
|
|
87e2b52a04 | ||
|
|
f36b62561a | ||
|
|
d2a146ead0 | ||
|
|
4926e53409 |
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -36,3 +36,9 @@
|
|||||||
|
|
||||||
# debug files
|
# debug files
|
||||||
__debug_*
|
__debug_*
|
||||||
|
|
||||||
|
# infisical
|
||||||
|
/.infisical.json
|
||||||
|
|
||||||
|
# traefik data
|
||||||
|
/traefik
|
||||||
|
|||||||
13
.zed/debug.json
Normal file
13
.zed/debug.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Attach to remote Delve",
|
||||||
|
"adapter": "Delve",
|
||||||
|
"mode": "remote",
|
||||||
|
"remotePath": "/tinyauth",
|
||||||
|
"request": "attach",
|
||||||
|
"tcp_connection": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 4000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
13
Dockerfile
13
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.3.6-alpine AS frontend-builder
|
FROM oven/bun:1.3.8-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -39,7 +39,10 @@ COPY ./cmd ./cmd
|
|||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.Version=${VERSION} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM alpine:3.23 AS runner
|
FROM alpine:3.23 AS runner
|
||||||
@@ -54,11 +57,9 @@ EXPOSE 3000
|
|||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENV DATABASEPATH=/data/tinyauth.db
|
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
||||||
|
|
||||||
ENV RESOURCESDIR=/data/resources
|
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||||
|
|
||||||
ENV GIN_MODE=release
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.3.6-alpine AS frontend-builder
|
FROM oven/bun:1.3.8-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
|||||||
|
|
||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.Version=${VERSION} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
|
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||||
@@ -61,8 +64,6 @@ ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
|
|||||||
|
|
||||||
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
ENV TINYAUTH_RESOURCESDIR=/data/resources
|
||||||
|
|
||||||
ENV GIN_MODE=release
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["tinyauth", "healthcheck"]
|
||||||
|
|||||||
25
Makefile
25
Makefile
@@ -18,6 +18,10 @@ deps:
|
|||||||
bun install --cwd frontend
|
bun install --cwd frontend
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
|
# Clean data
|
||||||
|
clean-data:
|
||||||
|
rm -rf data/
|
||||||
|
|
||||||
# Clean web UI build
|
# Clean web UI build
|
||||||
clean-webui:
|
clean-webui:
|
||||||
rm -rf internal/assets/dist
|
rm -rf internal/assets/dist
|
||||||
@@ -31,9 +35,9 @@ webui: clean-webui
|
|||||||
# Build the binary
|
# Build the binary
|
||||||
binary: webui
|
binary: webui
|
||||||
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
||||||
-X tinyauth/internal/config.Version=${TAG_NAME} \
|
-X github.com/steveiliop56/tinyauth/internal/config.Version=${TAG_NAME} \
|
||||||
-X tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
-o ${BIN_NAME} ./cmd/tinyauth
|
-o ${BIN_NAME} ./cmd/tinyauth
|
||||||
|
|
||||||
# Build for amd64
|
# Build for amd64
|
||||||
@@ -57,8 +61,21 @@ test:
|
|||||||
|
|
||||||
# Development
|
# Development
|
||||||
develop:
|
develop:
|
||||||
docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||||
|
|
||||||
|
# Development - Infisical
|
||||||
|
develop-infisical:
|
||||||
|
infisical run --env=dev -- docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
prod:
|
prod:
|
||||||
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
|
# Production - Infisical
|
||||||
|
prod-infisical:
|
||||||
|
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
|
# SQL
|
||||||
|
.PHONY: sql
|
||||||
|
sql:
|
||||||
|
sqlc generate
|
||||||
|
|||||||
@@ -28,14 +28,21 @@ func healthcheckCmd() *cli.Command {
|
|||||||
Run: func(args []string) error {
|
Run: func(args []string) error {
|
||||||
tlog.NewSimpleLogger().Init()
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
appUrl := os.Getenv("TINYAUTH_APPURL")
|
appUrl := "http://127.0.0.1:3000"
|
||||||
|
|
||||||
|
srvAddr := os.Getenv("TINYAUTH_SERVER_ADDRESS")
|
||||||
|
srvPort := os.Getenv("TINYAUTH_SERVER_PORT")
|
||||||
|
|
||||||
|
if srvAddr != "" && srvPort != "" {
|
||||||
|
appUrl = fmt.Sprintf("http://%s:%s", srvAddr, srvPort)
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
appUrl = args[0]
|
appUrl = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if appUrl == "" {
|
if appUrl == "" {
|
||||||
return errors.New("TINYAUTH_APPURL is not set and no argument was provided")
|
return errors.New("Could not determine app URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
|
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
},
|
},
|
||||||
Auth: config.AuthConfig{
|
Auth: config.AuthConfig{
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 86400, // 1 day
|
||||||
SessionMaxLifetime: 0,
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300,
|
LoginTimeout: 300, // 5 minutes
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
},
|
},
|
||||||
UI: config.UIConfig{
|
UI: config.UIConfig{
|
||||||
@@ -32,8 +32,9 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
BackgroundImage: "/background.jpg",
|
BackgroundImage: "/background.jpg",
|
||||||
},
|
},
|
||||||
Ldap: config.LdapConfig{
|
Ldap: config.LdapConfig{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
SearchFilter: "(uid=%s)",
|
SearchFilter: "(uid=%s)",
|
||||||
|
GroupCacheTTL: 900, // 15 minutes
|
||||||
},
|
},
|
||||||
Log: config.LogConfig{
|
Log: config.LogConfig{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
@@ -53,6 +54,10 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
OIDC: config.OIDCConfig{
|
||||||
|
PrivateKeyPath: "./tinyauth_oidc_key",
|
||||||
|
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||||
|
},
|
||||||
Experimental: config.ExperimentalConfig{
|
Experimental: config.ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
@@ -78,13 +83,23 @@ func main() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmdUser := &cli.Command{
|
||||||
|
Name: "user",
|
||||||
|
Description: "Utilities for creating and verifying Tinyauth users.",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdTotp := &cli.Command{
|
||||||
|
Name: "totp",
|
||||||
|
Description: "Utilities for creating Tinyauth TOTP users.",
|
||||||
|
}
|
||||||
|
|
||||||
err := cmdTinyauth.AddCommand(versionCmd())
|
err := cmdTinyauth.AddCommand(versionCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
log.Fatal().Err(err).Msg("Failed to add version command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(verifyUserCmd())
|
err = cmdUser.AddCommand(verifyUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
log.Fatal().Err(err).Msg("Failed to add verify command")
|
||||||
@@ -96,18 +111,30 @@ func main() {
|
|||||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(generateTotpCmd())
|
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
log.Fatal().Err(err).Msg("Failed to add generate command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(createUserCmd())
|
err = cmdUser.AddCommand(createUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
log.Fatal().Err(err).Msg("Failed to add create command")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(cmdUser)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to add user command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to add totp command")
|
||||||
|
}
|
||||||
|
|
||||||
err = cli.Execute(cmdTinyauth)
|
err = cli.Execute(cmdTinyauth)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
image: traefik:v3.3
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
@@ -50,3 +50,4 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik
|
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik
|
||||||
|
traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders: remote-user, remote-sub, remote-name, remote-email, remote-groups
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
image: traefik:v3.3
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
|
|||||||
@@ -12,72 +12,72 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.17",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^25.7.4",
|
"i18next": "^25.8.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "^16.5.3",
|
"react-i18next": "^16.5.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.12.0",
|
"react-router": "^7.13.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
"@tanstack/eslint-plugin-query": "^5.91.4",
|
||||||
"@types/node": "^25.0.8",
|
"@types/node": "^25.2.0",
|
||||||
"@types/react": "^19.2.8",
|
"@types/react": "^19.2.11",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.3.0",
|
||||||
"prettier": "3.8.0",
|
"prettier": "3.8.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.53.0",
|
"typescript-eslint": "^8.54.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||||
|
|
||||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||||
|
|
||||||
@@ -87,11 +87,11 @@
|
|||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||||
|
|
||||||
@@ -193,12 +193,6 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
|
||||||
|
|
||||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
@@ -263,7 +257,7 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="],
|
||||||
|
|
||||||
@@ -337,11 +331,11 @@
|
|||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw=="],
|
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.4", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.17", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
|
||||||
|
|
||||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||||
|
|
||||||
@@ -365,37 +359,37 @@
|
|||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
|
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
"@types/react": ["@types/react@19.2.11", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-tORuanb01iEzWvMGVGv2ZDhYZVeRMrw453DCSAIn/5yvcSVnMoUMTyf33nQJLahYEnv9xqrTNbgz4qY5EfSh0g=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="],
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="],
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="],
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="],
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="],
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.2", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
@@ -411,7 +405,7 @@
|
|||||||
|
|
||||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
|
||||||
|
|
||||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
@@ -419,8 +413,6 @@
|
|||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
@@ -503,7 +495,7 @@
|
|||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
|
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.0", "", { "peerDependencies": { "eslint": ">=9" } }, "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
@@ -525,20 +517,14 @@
|
|||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
|
||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
|
||||||
|
|
||||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
@@ -563,7 +549,7 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="],
|
"globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
@@ -589,7 +575,7 @@
|
|||||||
|
|
||||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||||
|
|
||||||
"i18next": ["i18next@25.7.4", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw=="],
|
"i18next": ["i18next@25.8.3", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-IC/pp2vkczdu1sBheq1eC92bLavN6fM5jH61c7Xa23PGio5ePEd+EP+re1IkO7KEM9eyeJHUxvIRxsaYTlsSyQ=="],
|
||||||
|
|
||||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||||
|
|
||||||
@@ -617,8 +603,6 @@
|
|||||||
|
|
||||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||||
|
|
||||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
@@ -675,7 +659,7 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
@@ -697,8 +681,6 @@
|
|||||||
|
|
||||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
|
||||||
|
|
||||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
@@ -741,8 +723,6 @@
|
|||||||
|
|
||||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
|
||||||
|
|
||||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
@@ -781,7 +761,7 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="],
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||||
|
|
||||||
@@ -789,15 +769,13 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
|
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
|
||||||
|
|
||||||
"react-i18next": ["react-i18next@16.5.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="],
|
"react-i18next": ["react-i18next@16.5.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g=="],
|
||||||
|
|
||||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||||
|
|
||||||
@@ -807,7 +785,7 @@
|
|||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="],
|
"react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
|
||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
@@ -817,12 +795,8 @@
|
|||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
|
||||||
|
|
||||||
"rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="],
|
"rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
@@ -857,8 +831,6 @@
|
|||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
@@ -873,7 +845,7 @@
|
|||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="],
|
"typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
@@ -915,33 +887,25 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="],
|
"@babel/generator/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
|
||||||
|
|
||||||
"@babel/helpers/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
|
||||||
|
|
||||||
"@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
"@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
"@babel/template/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
"@babel/template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/template/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
@@ -997,130 +961,112 @@
|
|||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
"@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="],
|
"@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
"eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
||||||
|
|
||||||
"hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||||
|
|
||||||
"i18next-resources-to-backend/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
"i18next-resources-to-backend/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="],
|
"@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
|
||||||
|
|
||||||
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
"@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
"@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="],
|
"@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="],
|
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
"typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,42 +18,42 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.17",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^25.7.4",
|
"i18next": "^25.8.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "^16.5.3",
|
"react-i18next": "^16.5.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.12.0",
|
"react-router": "^7.13.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
"@tanstack/eslint-plugin-query": "^5.91.4",
|
||||||
"@types/node": "^25.0.8",
|
"@types/node": "^25.2.0",
|
||||||
"@types/react": "^19.2.8",
|
"@types/react": "^19.2.11",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.3.0",
|
||||||
"prettier": "3.8.0",
|
"prettier": "3.8.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.53.0",
|
"typescript-eslint": "^8.54.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ import z from "zod";
|
|||||||
interface Props {
|
interface Props {
|
||||||
formId: string;
|
formId: string;
|
||||||
onSubmit: (code: TotpSchema) => void;
|
onSubmit: (code: TotpSchema) => void;
|
||||||
loading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TotpForm = (props: Props) => {
|
export const TotpForm = (props: Props) => {
|
||||||
const { formId, onSubmit, loading } = props;
|
const { formId, onSubmit } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
z.config({
|
z.config({
|
||||||
@@ -30,6 +29,14 @@ export const TotpForm = (props: Props) => {
|
|||||||
resolver: zodResolver(totpSchema),
|
resolver: zodResolver(totpSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
form.setValue("code", value, { shouldDirty: true, shouldValidate: true });
|
||||||
|
|
||||||
|
if (value.length === 6) {
|
||||||
|
onSubmit({ code: value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
@@ -41,10 +48,10 @@ export const TotpForm = (props: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<InputOTP
|
<InputOTP
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
disabled={loading}
|
|
||||||
{...field}
|
{...field}
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot index={0} />
|
<InputOTPSlot index={0} />
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ code {
|
|||||||
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
|
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
@apply bg-accent border border-border rounded-md p-2 whitespace-break-spaces;
|
||||||
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
@apply text-xl text-muted-foreground;
|
@apply text-xl text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|||||||
53
frontend/src/lib/hooks/oidc.ts
Normal file
53
frontend/src/lib/hooks/oidc.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export type OIDCValues = {
|
||||||
|
scope: string;
|
||||||
|
response_type: string;
|
||||||
|
client_id: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IuseOIDCParams {
|
||||||
|
values: OIDCValues;
|
||||||
|
compiled: string;
|
||||||
|
isOidc: boolean;
|
||||||
|
missingParams: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionalParams: string[] = ["state"];
|
||||||
|
|
||||||
|
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
|
||||||
|
let compiled: string = "";
|
||||||
|
let isOidc = false;
|
||||||
|
const missingParams: string[] = [];
|
||||||
|
|
||||||
|
const values: OIDCValues = {
|
||||||
|
scope: params.get("scope") ?? "",
|
||||||
|
response_type: params.get("response_type") ?? "",
|
||||||
|
client_id: params.get("client_id") ?? "",
|
||||||
|
redirect_uri: params.get("redirect_uri") ?? "",
|
||||||
|
state: params.get("state") ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(values)) {
|
||||||
|
if (!values[key as keyof OIDCValues]) {
|
||||||
|
if (!optionalParams.includes(key)) {
|
||||||
|
missingParams.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingParams.length === 0) {
|
||||||
|
isOidc = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOidc) {
|
||||||
|
compiled = new URLSearchParams(values).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
compiled,
|
||||||
|
isOidc,
|
||||||
|
missingParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
64
frontend/src/lib/hooks/redirect-uri.ts
Normal file
64
frontend/src/lib/hooks/redirect-uri.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
type IuseRedirectUri = {
|
||||||
|
url?: URL;
|
||||||
|
valid: boolean;
|
||||||
|
trusted: boolean;
|
||||||
|
allowedProto: boolean;
|
||||||
|
httpsDowngrade: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRedirectUri = (
|
||||||
|
redirect_uri: string | null,
|
||||||
|
cookieDomain: string,
|
||||||
|
): IuseRedirectUri => {
|
||||||
|
let isValid = false;
|
||||||
|
let isTrusted = false;
|
||||||
|
let isAllowedProto = false;
|
||||||
|
let isHttpsDowngrade = false;
|
||||||
|
|
||||||
|
if (!redirect_uri) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
trusted: false,
|
||||||
|
allowedProto: false,
|
||||||
|
httpsDowngrade: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(redirect_uri);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
trusted: false,
|
||||||
|
allowedProto: false,
|
||||||
|
httpsDowngrade: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
url.hostname == cookieDomain ||
|
||||||
|
url.hostname.endsWith(`.${cookieDomain}`)
|
||||||
|
) {
|
||||||
|
isTrusted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol == "http:" || url.protocol == "https:") {
|
||||||
|
isAllowedProto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.protocol == "https:" && url.protocol == "http:") {
|
||||||
|
isHttpsDowngrade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
valid: isValid,
|
||||||
|
trusted: isTrusted,
|
||||||
|
allowedProto: isAllowedProto,
|
||||||
|
httpsDowngrade: isHttpsDowngrade,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "نسيت كلمة المرور؟",
|
"forgotPasswordTitle": "نسيت كلمة المرور؟",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "حدث خطأ",
|
"errorTitle": "حدث خطأ",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "تجاهل",
|
"ignoreTitle": "تجاهل",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL",
|
"loginOauthFailSubtitle": "Nepodařilo se získat OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Přesměrování",
|
"loginOauthSuccessTitle": "Přesměrování",
|
||||||
"loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth",
|
"loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Automatické přesměrování OAuth",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Redirect now",
|
||||||
"continueTitle": "Pokračovat",
|
"continueTitle": "Pokračovat",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Zapomněli jste heslo?",
|
"forgotPasswordTitle": "Zapomněli jste heslo?",
|
||||||
"failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.",
|
"failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.",
|
||||||
"errorTitle": "Došlo k chybě",
|
"errorTitle": "Došlo k chybě",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.",
|
"errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.",
|
||||||
"forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.",
|
"forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.",
|
||||||
"fieldRequired": "Toto pole je povinné",
|
"fieldRequired": "Toto pole je povinné",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Glemt din adgangskode?",
|
"forgotPasswordTitle": "Glemt din adgangskode?",
|
||||||
"failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.",
|
"failedToFetchProvidersTitle": "Kunne ikke indlæse godkendelsesudbydere. Tjek venligst din konfiguration.",
|
||||||
"errorTitle": "Der opstod en fejl",
|
"errorTitle": "Der opstod en fejl",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.",
|
"errorSubtitle": "Der opstod en fejl under forsøget på at udføre denne handling. Tjek venligst konsollen for mere information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,17 +14,17 @@
|
|||||||
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
|
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL",
|
||||||
"loginOauthSuccessTitle": "Leite weiter",
|
"loginOauthSuccessTitle": "Leite weiter",
|
||||||
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
|
"loginOauthSuccessSubtitle": "Weiterleitung zu Ihrem OAuth-Provider",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Automatische OAuth-Weiterleitung",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Sie werden automatisch zu Ihrem OAuth-Anbieter weitergeleitet, um sich zu authentifizieren.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Jetzt weiterleiten",
|
||||||
"continueTitle": "Weiter",
|
"continueTitle": "Weiter",
|
||||||
"continueRedirectingTitle": "Leite weiter...",
|
"continueRedirectingTitle": "Leite weiter...",
|
||||||
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden",
|
"continueRedirectingSubtitle": "Sie sollten in Kürze zur App weitergeleitet werden",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Manuell weiterleiten",
|
||||||
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
|
"continueInsecureRedirectTitle": "Unsichere Weiterleitung",
|
||||||
"continueInsecureRedirectSubtitle": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?",
|
"continueInsecureRedirectSubtitle": "Sie versuchen von <code>https</code> auf <code>http</code> weiterzuleiten, was unsicher ist. Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung",
|
||||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
"continueUntrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt (<code>{{cookieDomain}}</code>). Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||||
"logoutFailTitle": "Abmelden fehlgeschlagen",
|
"logoutFailTitle": "Abmelden fehlgeschlagen",
|
||||||
"logoutFailSubtitle": "Bitte versuchen Sie es erneut",
|
"logoutFailSubtitle": "Bitte versuchen Sie es erneut",
|
||||||
"logoutSuccessTitle": "Abgemeldet",
|
"logoutSuccessTitle": "Abgemeldet",
|
||||||
@@ -51,12 +51,31 @@
|
|||||||
"forgotPasswordTitle": "Passwort vergessen?",
|
"forgotPasswordTitle": "Passwort vergessen?",
|
||||||
"failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.",
|
"failedToFetchProvidersTitle": "Fehler beim Laden der Authentifizierungsanbieter. Bitte überprüfen Sie Ihre Konfiguration.",
|
||||||
"errorTitle": "Ein Fehler ist aufgetreten",
|
"errorTitle": "Ein Fehler ist aufgetreten",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.",
|
"errorSubtitle": "Beim Versuch, diese Aktion auszuführen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole für weitere Informationen.",
|
||||||
"forgotPasswordMessage": "Das Passwort kann durch Änderung der 'USERS' Variable zurückgesetzt werden.",
|
"forgotPasswordMessage": "Das Passwort kann durch Änderung der 'USERS' Variable zurückgesetzt werden.",
|
||||||
"fieldRequired": "Dieses Feld ist notwendig",
|
"fieldRequired": "Dieses Feld ist notwendig",
|
||||||
"invalidInput": "Ungültige Eingabe",
|
"invalidInput": "Ungültige Eingabe",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Ungültige Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Diese Instanz ist so konfiguriert, dass sie von <code>{{appUrl}}</code> aufgerufen werden kann, aber <code>{{currentUrl}}</code> wird verwendet. Wenn Sie fortfahren, können Probleme bei der Authentifizierung auftreten.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignorieren",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Zur korrekten Domain gehen",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
|
"forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;",
|
||||||
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
|
"failedToFetchProvidersTitle": "Αποτυχία φόρτωσης παρόχων πιστοποίησης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας.",
|
||||||
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
|
"errorTitle": "Παρουσιάστηκε ένα σφάλμα",
|
||||||
|
"errorSubtitleInfo": "Το ακόλουθο σφάλμα προέκυψε κατά την επεξεργασία του αιτήματός σας:",
|
||||||
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.",
|
"errorSubtitle": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια εκτέλεσης αυτής της ενέργειας. Ελέγξτε την κονσόλα για περισσότερες πληροφορίες.",
|
||||||
"forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.",
|
"forgotPasswordMessage": "Μπορείτε να επαναφέρετε τον κωδικό πρόσβασής σας αλλάζοντας τη μεταβλητή περιβάλλοντος `USERS`.",
|
||||||
"fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό",
|
"fieldRequired": "Αυτό το πεδίο είναι υποχρεωτικό",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Μη έγκυρο domain",
|
"domainWarningTitle": "Μη έγκυρο domain",
|
||||||
"domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από <code>{{appUrl}}</code>, αλλά <code>{{currentUrl}}</code> χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.",
|
"domainWarningSubtitle": "Αυτή η εφαρμογή έχει ρυθμιστεί για πρόσβαση από <code>{{appUrl}}</code>, αλλά <code>{{currentUrl}}</code> χρησιμοποιείται. Αν συνεχίσετε, μπορεί να αντιμετωπίσετε προβλήματα με την ταυτοποίηση.",
|
||||||
"ignoreTitle": "Παράβλεψη",
|
"ignoreTitle": "Παράβλεψη",
|
||||||
"goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain"
|
"goToCorrectDomainTitle": "Μεταβείτε στο σωστό domain",
|
||||||
}
|
"authorizeTitle": "Εξουσιοδότηση",
|
||||||
|
"authorizeCardTitle": "Συνέχεια στην εφαρμογή {{app}};",
|
||||||
|
"authorizeSubtitle": "Θα θέλατε να συνεχίσετε σε αυτή την εφαρμογή; Παρακαλώ ελέγξτε προσεκτικά τα δικαιώματα που ζητούνται από την εφαρμογή.",
|
||||||
|
"authorizeSubtitleOAuth": "Θα θέλατε να συνεχίσετε σε αυτή την εφαρμογή;",
|
||||||
|
"authorizeLoadingTitle": "Φόρτωση...",
|
||||||
|
"authorizeLoadingSubtitle": "Παρακαλώ περιμένετε όσο φορτώνουμε τις απαραίτητες πληροφορίες.",
|
||||||
|
"authorizeSuccessTitle": "Εξουσιοδοτημένος",
|
||||||
|
"authorizeSuccessSubtitle": "Θα μεταφερθείτε στην εφαρμογή σε λίγα δευτερόλεπτα.",
|
||||||
|
"authorizeErrorClientInfo": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των πληροφοριών. Παρακαλώ προσπαθήστε ξανά αργότερα.",
|
||||||
|
"authorizeErrorMissingParams": "Οι παρακάτω απαραίτητες πληροφορίες λείπουν από το αίτημά σας: {{missingParams}}",
|
||||||
|
"openidScopeName": "Σύνδεση OpenID",
|
||||||
|
"openidScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες σύνδεσης OpenID.",
|
||||||
|
"emailScopeName": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
|
||||||
|
"emailScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στη διεύθυνση ηλεκτρονικού ταχυδρομείου σας.",
|
||||||
|
"profileScopeName": "Προφίλ",
|
||||||
|
"profileScopeDescription": "Επιτρέπει στην εφαρμογή να έχει πρόσβαση στις πληροφορίες του προφίλ σας.",
|
||||||
|
"groupsScopeName": "Ομάδες",
|
||||||
|
"groupsScopeDescription": "Επιτρέπει στην εφαρμογή την πρόσβαση στις πληροφορίες ομάδας σας."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,12 +51,31 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,12 +51,31 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "¿Olvidó su contraseña?",
|
"forgotPasswordTitle": "¿Olvidó su contraseña?",
|
||||||
"failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.",
|
"failedToFetchProvidersTitle": "Error al cargar los proveedores de autenticación. Por favor revise su configuración.",
|
||||||
"errorTitle": "Ha ocurrido un error",
|
"errorTitle": "Ha ocurrido un error",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.",
|
"errorSubtitle": "Ocurrió un error mientras se trataba de realizar esta acción. Por favor, revise la consola para más información.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Unohditko salasanasi?",
|
"forgotPasswordTitle": "Unohditko salasanasi?",
|
||||||
"failedToFetchProvidersTitle": "Todennuspalvelujen tarjoajien lataaminen epäonnistui. Tarkista määrityksesi.",
|
"failedToFetchProvidersTitle": "Todennuspalvelujen tarjoajien lataaminen epäonnistui. Tarkista määrityksesi.",
|
||||||
"errorTitle": "Tapahtui virhe",
|
"errorTitle": "Tapahtui virhe",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Tapahtui virhe yritettäessä suorittaa tämä toiminto. Ole hyvä ja tarkista konsoli saadaksesi lisätietoja.",
|
"errorSubtitle": "Tapahtui virhe yritettäessä suorittaa tämä toiminto. Ole hyvä ja tarkista konsoli saadaksesi lisätietoja.",
|
||||||
"forgotPasswordMessage": "Voit nollata salasanasi vaihtamalla ympäristömuuttujan `USERS`.",
|
"forgotPasswordMessage": "Voit nollata salasanasi vaihtamalla ympäristömuuttujan `USERS`.",
|
||||||
"fieldRequired": "Tämä kenttä on pakollinen",
|
"fieldRequired": "Tämä kenttä on pakollinen",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Virheellinen verkkotunnus",
|
"domainWarningTitle": "Virheellinen verkkotunnus",
|
||||||
"domainWarningSubtitle": "Tämä instanssi on määritelty käyttämään osoitetta <code>{{appUrl}}</code>, mutta nykyinen osoite on <code>{{currentUrl}}</code>. Jos jatkat, saatat törmätä ongelmiin autentikoinnissa.",
|
"domainWarningSubtitle": "Tämä instanssi on määritelty käyttämään osoitetta <code>{{appUrl}}</code>, mutta nykyinen osoite on <code>{{currentUrl}}</code>. Jos jatkat, saatat törmätä ongelmiin autentikoinnissa.",
|
||||||
"ignoreTitle": "Jätä huomiotta",
|
"ignoreTitle": "Jätä huomiotta",
|
||||||
"goToCorrectDomainTitle": "Siirry oikeaan verkkotunnukseen"
|
"goToCorrectDomainTitle": "Siirry oikeaan verkkotunnukseen",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Mot de passe oublié ?",
|
"forgotPasswordTitle": "Mot de passe oublié ?",
|
||||||
"failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.",
|
"failedToFetchProvidersTitle": "Échec du chargement des fournisseurs d'authentification. Veuillez vérifier votre configuration.",
|
||||||
"errorTitle": "Une erreur est survenue",
|
"errorTitle": "Une erreur est survenue",
|
||||||
|
"errorSubtitleInfo": "L'erreur suivante s'est produite lors du traitement de votre requête :",
|
||||||
"errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.",
|
"errorSubtitle": "Une erreur est survenue lors de l'exécution de cette action. Veuillez consulter la console pour plus d'informations.",
|
||||||
"forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.",
|
"forgotPasswordMessage": "Vous pouvez réinitialiser votre mot de passe en modifiant la variable d'environnement `USERS`.",
|
||||||
"fieldRequired": "Ce champ est obligatoire",
|
"fieldRequired": "Ce champ est obligatoire",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Domaine invalide",
|
"domainWarningTitle": "Domaine invalide",
|
||||||
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
|
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
|
||||||
"ignoreTitle": "Ignorer",
|
"ignoreTitle": "Ignorer",
|
||||||
"goToCorrectDomainTitle": "Aller au bon domaine"
|
"goToCorrectDomainTitle": "Aller au bon domaine",
|
||||||
}
|
"authorizeTitle": "Autoriser",
|
||||||
|
"authorizeCardTitle": "Continuer vers {{app}} ?",
|
||||||
|
"authorizeSubtitle": "Voulez-vous continuer vers cette application ? Veuillez examiner attentivement les autorisations demandées par l'application.",
|
||||||
|
"authorizeSubtitleOAuth": "Voulez-vous continuer vers cette application ?",
|
||||||
|
"authorizeLoadingTitle": "Chargement...",
|
||||||
|
"authorizeLoadingSubtitle": "Veuillez patienter pendant que nous chargeons les informations du client.",
|
||||||
|
"authorizeSuccessTitle": "Autorisé",
|
||||||
|
"authorizeSuccessSubtitle": "Vous allez être redirigé vers l'application dans quelques secondes.",
|
||||||
|
"authorizeErrorClientInfo": "Une erreur est survenue lors du chargement des informations du client. Veuillez réessayer plus tard.",
|
||||||
|
"authorizeErrorMissingParams": "Les paramètres suivants sont manquants : {{missingParams}}",
|
||||||
|
"openidScopeName": "Connexion OpenID",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Autorise l'application à accéder à votre adresse e-mail.",
|
||||||
|
"profileScopeName": "Profil",
|
||||||
|
"profileScopeDescription": "Autorise l'application à accéder aux informations de votre profil.",
|
||||||
|
"groupsScopeName": "Groupes",
|
||||||
|
"groupsScopeDescription": "Autorise une application à accéder aux informations de votre groupe."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Welcome back, login with",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Üdvözöljük, kérem jelentkezzen be",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Vagy",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Felhasználónév",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Jelszó",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Bejelentkezés",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Sikertelen bejelentkezés",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Kérjük, ellenőrizze a felhasználónevét és jelszavát",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Túl sokszor próbálkoztál bejelentkezni. Próbáld újra később",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Bejelentkezve",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Üdvözöljük!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "An error occurred",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Átirányítás",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Redirect now",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Continue",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Átirányítás...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Redirect me manually",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Insecure redirect",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
||||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Sikertelen kijelentkezés",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Próbálja újra",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Kijelentkezve",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Kijelentkeztél",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Kijelentkezés",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Page not found",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Ugrás a kezdőlapra",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Érvénytelen kód",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Kérjük ellenőrizze a kódot és próbálja újra",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Verified",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
@@ -46,17 +46,36 @@
|
|||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Próbálja újra",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Mégse",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Elfelejtette jelszavát?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Hiba történt",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Ez egy kötelező mező",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Bentornato, accedi con",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Bentornato, accedi al tuo account",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Oppure",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Nome utente",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Password",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Accesso",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Accesso non riuscito",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Verifica che il nome utente e la password siano corretti",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Hai effettuato troppi tentativi errati. Riprova più tardi",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Accesso effettuato",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Bentornato!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Si è verificato un errore",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Impossibile ottenere l'URL di OAuth",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Redirecting",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Redirect now",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Prosegui",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Redirecting...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Redirect me manually",
|
||||||
@@ -34,29 +34,48 @@
|
|||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Page not found",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "The page you are looking for does not exist.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Vai alla home",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Errore nella verifica del codice",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Si prega di controllare il codice e riprovare",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Verificato",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Reindirizzamento alla tua app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Inserisci il tuo codice TOTP",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Inserisci il codice dalla tua app di autenticazione.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Non Autorizzato",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "L'utente con username <code>{{username}}</code> non è autorizzato ad accedere alla risorsa <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "L'utente con username <code>{{username}}</code> non è autorizzato a effettuare l'accesso.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "L'utente con nome utente <code>{{username}}</code> non fa parte dei gruppi richiesti dalla risorsa <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Il tuo indirizzo IP <code>{{ip}}</code> non è autorizzato ad accedere alla risorsa <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Riprova",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Annulla",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Password dimenticata?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Impossibile caricare i provider di autenticazione. Si prega di controllare la configurazione.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Si è verificato un errore",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Puoi reimpostare la tua password modificando la variabile d'ambiente `USERS`.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Questo campo è obbligatorio",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Input non valido",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Dominio non valido",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Questa istanza è configurata per essere accessibile da <code>{{appUrl}}</code>, ma <code>{{currentUrl}}</code> è in uso. Se procedi, potresti incorrere in problemi di autenticazione.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignora",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Vai al dominio corretto",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welkom terug, log in met",
|
"loginTitle": "Welkom terug, log in met",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Welkom terug, log in",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Of",
|
||||||
"loginUsername": "Gebruikersnaam",
|
"loginUsername": "Gebruikersnaam",
|
||||||
"loginPassword": "Wachtwoord",
|
"loginPassword": "Wachtwoord",
|
||||||
"loginSubmit": "Log in",
|
"loginSubmit": "Log in",
|
||||||
"loginFailTitle": "Mislukt om in te loggen",
|
"loginFailTitle": "Mislukt om in te loggen",
|
||||||
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord",
|
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Inloggen is te vaak mislukt. Probeer het later opnieuw",
|
||||||
"loginSuccessTitle": "Ingelogd",
|
"loginSuccessTitle": "Ingelogd",
|
||||||
"loginSuccessSubtitle": "Welkom terug!",
|
"loginSuccessSubtitle": "Welkom terug!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Er is een fout opgetreden",
|
||||||
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL",
|
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Omleiden",
|
"loginOauthSuccessTitle": "Omleiden",
|
||||||
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider",
|
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "OAuth automatische omleiding",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Je wordt automatisch omgeleid naar je OAuth provider om te authenticeren.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Nu omleiden",
|
||||||
"continueTitle": "Ga verder",
|
"continueTitle": "Ga verder",
|
||||||
"continueRedirectingTitle": "Omleiden...",
|
"continueRedirectingTitle": "Omleiden...",
|
||||||
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd",
|
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Stuur mij handmatig door",
|
||||||
"continueInsecureRedirectTitle": "Onveilige doorverwijzing",
|
"continueInsecureRedirectTitle": "Onveilige doorverwijzing",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "Je probeert door te verwijzen van <code>https</code> naar <code>http</code> die niet veilig is. Weet je zeker dat je wilt doorgaan?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Niet-vertrouwde doorverwijzing",
|
||||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
"continueUntrustedRedirectSubtitle": "Je probeert door te sturen naar een domein dat niet overeenkomt met je geconfigureerde domein (<code>{{cookieDomain}}</code>). Weet je zeker dat je wilt doorgaan?",
|
||||||
"logoutFailTitle": "Afmelden mislukt",
|
"logoutFailTitle": "Afmelden mislukt",
|
||||||
"logoutFailSubtitle": "Probeer het opnieuw",
|
"logoutFailSubtitle": "Probeer het opnieuw",
|
||||||
"logoutSuccessTitle": "Afgemeld",
|
"logoutSuccessTitle": "Afgemeld",
|
||||||
"logoutSuccessSubtitle": "Je bent afgemeld",
|
"logoutSuccessSubtitle": "Je bent afgemeld",
|
||||||
"logoutTitle": "Afmelden",
|
"logoutTitle": "Afmelden",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "Je bent momenteel ingelogd als <code>{{username}}</code>. Klik op de onderstaande knop om uit te loggen.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Je bent momenteel ingelogd als <code>{{username}}</code> met behulp van de {{provider}} OAuth provider. Klik op de onderstaande knop om uit te loggen.",
|
||||||
"notFoundTitle": "Pagina niet gevonden",
|
"notFoundTitle": "Pagina niet gevonden",
|
||||||
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.",
|
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.",
|
||||||
"notFoundButton": "Naar startpagina",
|
"notFoundButton": "Naar startpagina",
|
||||||
@@ -40,23 +40,42 @@
|
|||||||
"totpSuccessTitle": "Geverifiëerd",
|
"totpSuccessTitle": "Geverifiëerd",
|
||||||
"totpSuccessSubtitle": "Omleiden naar je app",
|
"totpSuccessSubtitle": "Omleiden naar je app",
|
||||||
"totpTitle": "Voer je TOTP-code in",
|
"totpTitle": "Voer je TOTP-code in",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Voer de code van je authenticator-app in.",
|
||||||
"unauthorizedTitle": "Ongeautoriseerd",
|
"unauthorizedTitle": "Ongeautoriseerd",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <code>{{username}}</code> is niet gemachtigd om de bron <code>{{resource}}</code> te gebruiken.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "De gebruiker met gebruikersnaam <code>{{username}}</code> is niet gemachtigd om in te loggen.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "De gebruiker met gebruikersnaam <code>{{username}}</code> maakt geen deel uit van de groepen die vereist zijn door de bron <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Jouw IP-adres <code>{{ip}}</code> is niet gemachtigd om de bron <code>{{resource}}</code> te gebruiken.",
|
||||||
"unauthorizedButton": "Opnieuw proberen",
|
"unauthorizedButton": "Opnieuw proberen",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Annuleren",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Wachtwoord vergeten?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Fout bij het laden van de authenticatie-providers. Controleer je configuratie.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Er is een fout opgetreden",
|
||||||
|
"errorSubtitleInfo": "De volgende fout is opgetreden bij het verwerken van het verzoek:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Je kunt je wachtwoord opnieuw instellen door de `USERS` omgevingsvariabele te wijzigen.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Dit veld is verplicht",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Ongeldige invoer",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Ongeldig domein",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Deze instantie is geconfigureerd voor toegang tot <code>{{appUrl}}</code>, maar <code>{{currentUrl}}</code> wordt gebruikt. Als je doorgaat, kun je problemen ondervinden met authenticatie.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Negeren",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Ga naar het juiste domein",
|
||||||
}
|
"authorizeTitle": "Autoriseren",
|
||||||
|
"authorizeCardTitle": "Doorgaan naar {{app}}?",
|
||||||
|
"authorizeSubtitle": "Doorgaan naar deze app? Controleer de machtigingen die door de app worden gevraagd.",
|
||||||
|
"authorizeSubtitleOAuth": "Doorgaan naar deze app?",
|
||||||
|
"authorizeLoadingTitle": "Laden...",
|
||||||
|
"authorizeLoadingSubtitle": "Even geduld bij het laden van de cliëntinformatie.",
|
||||||
|
"authorizeSuccessTitle": "Geautoriseerd",
|
||||||
|
"authorizeSuccessSubtitle": "Je wordt binnen enkele seconden doorgestuurd naar de app.",
|
||||||
|
"authorizeErrorClientInfo": "Er is een fout opgetreden tijdens het laden van de cliëntinformatie. Probeer het later opnieuw.",
|
||||||
|
"authorizeErrorMissingParams": "De volgende parameters ontbreken: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Hiermee kan de app toegang krijgen tot jouw OpenID Connect-informatie.",
|
||||||
|
"emailScopeName": "E-mail",
|
||||||
|
"emailScopeDescription": "Hiermee kan de app toegang krijgen tot jouw e-mailadres.",
|
||||||
|
"profileScopeName": "Profiel",
|
||||||
|
"profileScopeDescription": "Hiermee kan de app toegang krijgen tot je profielinformatie.",
|
||||||
|
"groupsScopeName": "Groepen",
|
||||||
|
"groupsScopeDescription": "Hiermee kan de app toegang krijgen tot jouw groepsinformatie."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Nie pamiętasz hasła?",
|
"forgotPasswordTitle": "Nie pamiętasz hasła?",
|
||||||
"failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.",
|
"failedToFetchProvidersTitle": "Nie udało się załadować dostawców uwierzytelniania. Sprawdź swoją konfigurację.",
|
||||||
"errorTitle": "Wystąpił błąd",
|
"errorTitle": "Wystąpił błąd",
|
||||||
|
"errorSubtitleInfo": "Podczas przetwarzania żądania wystąpił następujący błąd:",
|
||||||
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.",
|
"errorSubtitle": "Wystąpił błąd podczas próby wykonania tej czynności. Sprawdź konsolę, aby uzyskać więcej informacji.",
|
||||||
"forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.",
|
"forgotPasswordMessage": "Możesz zresetować hasło, zmieniając zmienną środowiskową `USERS`.",
|
||||||
"fieldRequired": "To pole jest wymagane",
|
"fieldRequired": "To pole jest wymagane",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Nieprawidłowa domena",
|
"domainWarningTitle": "Nieprawidłowa domena",
|
||||||
"domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z <code>{{appUrl}}</code>, ale <code>{{currentUrl}}</code> jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.",
|
"domainWarningSubtitle": "Ta instancja jest skonfigurowana do uzyskania dostępu z <code>{{appUrl}}</code>, ale <code>{{currentUrl}}</code> jest w użyciu. Jeśli będziesz kontynuować, mogą wystąpić problemy z uwierzytelnianiem.",
|
||||||
"ignoreTitle": "Zignoruj",
|
"ignoreTitle": "Zignoruj",
|
||||||
"goToCorrectDomainTitle": "Przejdź do prawidłowej domeny"
|
"goToCorrectDomainTitle": "Przejdź do prawidłowej domeny",
|
||||||
}
|
"authorizeTitle": "Autoryzuj",
|
||||||
|
"authorizeCardTitle": "Kontynuować do {{app}}?",
|
||||||
|
"authorizeSubtitle": "Czy chcesz kontynuować do tej aplikacji? Uważnie zapoznaj się z uprawnieniami żądanymi przez aplikację.",
|
||||||
|
"authorizeSubtitleOAuth": "Czy chcesz kontynuować do tej aplikacji?",
|
||||||
|
"authorizeLoadingTitle": "Wczytywanie...",
|
||||||
|
"authorizeLoadingSubtitle": "Proszę czekać, aż załadujemy informacje o kliencie.",
|
||||||
|
"authorizeSuccessTitle": "Autoryzowano",
|
||||||
|
"authorizeSuccessSubtitle": "Za kilka sekund nastąpi przekierowanie do aplikacji.",
|
||||||
|
"authorizeErrorClientInfo": "Wystąpił błąd podczas ładowania informacji o kliencie. Spróbuj ponownie później.",
|
||||||
|
"authorizeErrorMissingParams": "Brakuje następujących parametrów: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Zezwala aplikacji na dostęp do informacji o OpenID Connect.",
|
||||||
|
"emailScopeName": "E-mail",
|
||||||
|
"emailScopeDescription": "Zezwala aplikacji na dostęp do adresów e-mail.",
|
||||||
|
"profileScopeName": "Profil",
|
||||||
|
"profileScopeDescription": "Zezwala aplikacji na dostęp do informacji o porfilu.",
|
||||||
|
"groupsScopeName": "Grupy",
|
||||||
|
"groupsScopeDescription": "Zezwala aplikacji na dostęp do informacji o grupie."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Esqueceu sua senha?",
|
"forgotPasswordTitle": "Esqueceu sua senha?",
|
||||||
"failedToFetchProvidersTitle": "Falha ao carregar provedores de autenticação. Verifique sua configuração.",
|
"failedToFetchProvidersTitle": "Falha ao carregar provedores de autenticação. Verifique sua configuração.",
|
||||||
"errorTitle": "Ocorreu um erro",
|
"errorTitle": "Ocorreu um erro",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Por favor, verifique o console para mais informações.",
|
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Por favor, verifique o console para mais informações.",
|
||||||
"forgotPasswordMessage": "Você pode redefinir sua senha alterando a variável de ambiente `USERS`.",
|
"forgotPasswordMessage": "Você pode redefinir sua senha alterando a variável de ambiente `USERS`.",
|
||||||
"fieldRequired": "Este campo é obrigatório",
|
"fieldRequired": "Este campo é obrigatório",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Domínio inválido",
|
"domainWarningTitle": "Domínio inválido",
|
||||||
"domainWarningSubtitle": "Esta instância está configurada para ser acessada de <code>{{appUrl}}</code>, mas <code>{{currentUrl}}</code> está sendo usado. Se você continuar, você pode encontrar problemas com a autenticação.",
|
"domainWarningSubtitle": "Esta instância está configurada para ser acessada de <code>{{appUrl}}</code>, mas <code>{{currentUrl}}</code> está sendo usado. Se você continuar, você pode encontrar problemas com a autenticação.",
|
||||||
"ignoreTitle": "Ignorar",
|
"ignoreTitle": "Ignorar",
|
||||||
"goToCorrectDomainTitle": "Ir para o domínio correto"
|
"goToCorrectDomainTitle": "Ir para o domínio correto",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Bem-vindo de volta, inicia sessão com",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Bem-vindo de volta, inicia sessão",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Ou",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Nome de utilizador",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Palavra-passe",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Iniciar sessão",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Falha ao iniciar sessão",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Verifica o nome de utilizador e a palavra-passe",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Falhaste o início de sessão demasiadas vezes. Tenta novamente mais tarde",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Sessão iniciada",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Bem-vindo de volta!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Ocorreu um erro",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Não foi possível obter o URL OAuth",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "A redirecionar",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "A redirecionar para o teu fornecedor OAuth",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Redirecionamento automático OAuth",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Vais ser redirecionado automaticamente para o teu fornecedor OAuth para autenticação.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Redirecionar agora",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Continuar",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "A redirecionar...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "Deverás ser redirecionado para a aplicação em breve",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Redirecionar manualmente",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Redirecionamento inseguro",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "Estás a tentar redirecionar de <code>https</code> para <code>http</code>, o que não é seguro. Tens a certeza de que queres continuar?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Redirecionamento não fidedigno",
|
||||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
"continueUntrustedRedirectSubtitle": "Estás a tentar redirecionar para um domínio que não corresponde ao domínio configurado (<code>{{cookieDomain}}</code>). Tens a certeza de que queres continuar?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Falha ao terminar sessão",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Tenta novamente",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Sessão terminada",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Terminaste a sessão com sucesso",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Terminar sessão",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "Estás com sessão iniciada como <code>{{username}}</code>. Clica no botão abaixo para terminar sessão.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Estás com sessão iniciada como <code>{{username}}</code> através do fornecedor OAuth {{provider}}. Clica no botão abaixo para terminar sessão.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Página não encontrada",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "A página que procuras não existe.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "Ir para o início",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Falha na verificação do código",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Verifica o código e tenta novamente",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Verificado",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "A redirecionar para a tua aplicação",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Introduz o teu código TOTP",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Introduz o código da tua aplicação de autenticação.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Não autorizado",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "O utilizador com o nome <code>{{username}}</code> não tem autorização para aceder ao recurso <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "O utilizador com o nome <code>{{username}}</code> não tem autorização para iniciar sessão.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "O utilizador com o nome <code>{{username}}</code> não pertence aos grupos exigidos pelo recurso <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "O teu endereço IP <code>{{ip}}</code> não tem autorização para aceder ao recurso <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Tentar novamente",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Cancelar",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Esqueceste-te da palavra-passe?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Falha ao carregar os fornecedores de autenticação. Verifica a configuração.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Ocorreu um erro",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"errorSubtitle": "Ocorreu um erro ao tentar executar esta ação. Consulta a consola para mais informações.",
|
||||||
"fieldRequired": "This field is required",
|
"forgotPasswordMessage": "Podes redefinir a tua palavra-passe alterando a variável de ambiente `USERS`.",
|
||||||
"invalidInput": "Invalid input",
|
"fieldRequired": "Este campo é obrigatório",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"invalidInput": "Entrada inválida",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningTitle": "Domínio inválido",
|
||||||
"ignoreTitle": "Ignore",
|
"domainWarningSubtitle": "Esta instância está configurada para ser acedida a partir de <code>{{appUrl}}</code>, mas está a ser usado <code>{{currentUrl}}</code>. Se continuares, poderás ter problemas de autenticação.",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"ignoreTitle": "Ignorar",
|
||||||
}
|
"goToCorrectDomainTitle": "Ir para o domínio correto",
|
||||||
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Забыли пароль?",
|
"forgotPasswordTitle": "Забыли пароль?",
|
||||||
"failedToFetchProvidersTitle": "Не удалось загрузить поставщика авторизации. Пожалуйста, проверьте конфигурацию.",
|
"failedToFetchProvidersTitle": "Не удалось загрузить поставщика авторизации. Пожалуйста, проверьте конфигурацию.",
|
||||||
"errorTitle": "Произошла ошибка",
|
"errorTitle": "Произошла ошибка",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.",
|
"errorSubtitle": "Произошла ошибка при попытке выполнить это действие. Проверьте консоль для дополнительной информации.",
|
||||||
"forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.",
|
"forgotPasswordMessage": "Вы можете сбросить свой пароль, изменив переменную окружения `USERS`.",
|
||||||
"fieldRequired": "Это поле является обязательным",
|
"fieldRequired": "Это поле является обязательным",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Неверный домен",
|
"domainWarningTitle": "Неверный домен",
|
||||||
"domainWarningSubtitle": "Этот экземпляр настроен на доступ к нему из <code>{{appUrl}}</code>, но <code>{{currentUrl}}</code> в настоящее время используется. Если вы продолжите, то могут возникнуть проблемы с авторизацией.",
|
"domainWarningSubtitle": "Этот экземпляр настроен на доступ к нему из <code>{{appUrl}}</code>, но <code>{{currentUrl}}</code> в настоящее время используется. Если вы продолжите, то могут возникнуть проблемы с авторизацией.",
|
||||||
"ignoreTitle": "Игнорировать",
|
"ignoreTitle": "Игнорировать",
|
||||||
"goToCorrectDomainTitle": "Перейти к правильному домену"
|
"goToCorrectDomainTitle": "Перейти к правильному домену",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Заборавили сте лозинку?",
|
"forgotPasswordTitle": "Заборавили сте лозинку?",
|
||||||
"failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.",
|
"failedToFetchProvidersTitle": "Није успело учитавање провајдера аутентификације. Молим вас проверите ваша подешавања.",
|
||||||
"errorTitle": "Појавила се грешка",
|
"errorTitle": "Појавила се грешка",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.",
|
"errorSubtitle": "Појавила се грешка при покушају извршавања ове радње. Молим вас проверите конзолу за додатне информације.",
|
||||||
"forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.",
|
"forgotPasswordMessage": "Можете поништити вашу лозинку променом `USERS` променљиве окружења.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Forgot your password?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Tekrar Hoş Geldiniz, giriş yapın",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "Tekrar hoş geldiniz, lütfen giriş yapın",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Ya da",
|
||||||
"loginUsername": "Kullanıcı Adı",
|
"loginUsername": "Kullanıcı Adı",
|
||||||
"loginPassword": "Şifre",
|
"loginPassword": "Şifre",
|
||||||
"loginSubmit": "Giriş Yap",
|
"loginSubmit": "Giriş Yap",
|
||||||
"loginFailTitle": "Giriş yapılamadı",
|
"loginFailTitle": "Giriş yapılamadı",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Lütfen kullanıcı adınızı ve şifrenizi kontrol edin",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Çok fazla kez giriş yapma girişiminde bulundunuz. Lütfen daha sonra tekrar deneyin",
|
||||||
"loginSuccessTitle": "Giriş yapıldı",
|
"loginSuccessTitle": "Giriş yapıldı",
|
||||||
"loginSuccessSubtitle": "Tekrar hoş geldiniz!",
|
"loginSuccessSubtitle": "Tekrar hoş geldiniz!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Hata oluştu",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "OAuth URL'si alınamadı",
|
||||||
"loginOauthSuccessTitle": "Yönlendiriliyor",
|
"loginOauthSuccessTitle": "Yönlendiriliyor",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "OAuth sağlayıcınıza yönlendiriliyor",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "OAuth Otomatik Yönlendirme",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Kimlik doğrulama işlemi için otomatik olarak OAuth sağlayıcınıza yönlendirileceksiniz.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Şimdi Yönlendir",
|
||||||
"continueTitle": "Devam et",
|
"continueTitle": "Devam et",
|
||||||
"continueRedirectingTitle": "Yönlendiriliyor...",
|
"continueRedirectingTitle": "Yönlendiriliyor...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "Kısa süre içinde uygulamaya yönlendirileceksiniz",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Beni manuel olarak yönlendir",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Güvenli olmayan yönlendirme",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "<code>http</code> adresinden <code>http</code> adresine yönlendirme yapmaya çalışıyorsunuz, bu güvenli değil. Devam etmek istediğinizden emin misiniz?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Güvenilmeyen yönlendirme",
|
||||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
"continueUntrustedRedirectSubtitle": "Yapılandırdığınız alan adıyla eşleşmeyen bir alana yönlendirme yapmaya çalışıyorsunuz (<code>{{cookieDomain}}</code>). Devam etmek istediğinize emin misiniz?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Çıkış Yapılamadı",
|
||||||
"logoutFailSubtitle": "Lütfen tekrar deneyin",
|
"logoutFailSubtitle": "Lütfen tekrar deneyin",
|
||||||
"logoutSuccessTitle": "Çıkış yapıldı",
|
"logoutSuccessTitle": "Çıkış yapıldı",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Çıkış yaptınız",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Çıkış yap",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "<code>{{username}}</code> olarak giriş yapmış durumdasınız. Çıkış yapmak için aşağıdaki düğmeye tıklayın.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Şu anda {{provider}} OAuth sağlayıcısını kullanarak <code>{{username}}</code> olarak oturum açmış durumdasınız. Oturumunuzu kapatmak için aşağıdaki düğmeye tıklayın.",
|
||||||
"notFoundTitle": "Sayfa bulunamadı",
|
"notFoundTitle": "Sayfa bulunamadı",
|
||||||
"notFoundSubtitle": "Aradığınız sayfa mevcut değil.",
|
"notFoundSubtitle": "Aradığınız sayfa mevcut değil.",
|
||||||
"notFoundButton": "Ana sayfaya git",
|
"notFoundButton": "Ana sayfaya git",
|
||||||
"totpFailTitle": "Kod doğrulanamadı",
|
"totpFailTitle": "Kod doğrulanamadı",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Lütfen kodunuzu kontrol edin ve tekrar deneyin",
|
||||||
"totpSuccessTitle": "Doğrulandı",
|
"totpSuccessTitle": "Doğrulandı",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Uygulamanıza yönlendiriliyor",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "TOTP kodunuzu girin",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Lütfen kimlik doğrulama uygulamanızdan aldığınız kodu girin.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Yetkisiz",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "Kullanıcı adı <code>{{username}}</code> olan kullanıcının <code>{{resource}}</code> kaynağına erişim yetkisi bulunmamaktadır.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "Kullanıcı adı <code>{{username}}</code> olan kullanıcının oturum açma yetkisi yok.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "Kullanıcı adı <code>{{username}}</code> olan kullanıcı, <code>{{resource}}</code> kaynağının gerektirdiği gruplarda bulunmuyor.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "IP adresiniz <code>{{ip}}</code>, <code>{{resource}}</code> kaynağına erişim yetkisine sahip değil.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Tekrar deneyin",
|
||||||
"cancelTitle": "İptal",
|
"cancelTitle": "İptal",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Şifrenizi mi unuttunuz?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Kimlik doğrulama sağlayıcıları yüklenemedi. Lütfen yapılandırmanızı kontrol edin.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Bir hata oluştu",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Parolanızı `USERS` ortam değişkenini değiştirerek sıfırlayabilirsiniz.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Bu alan zorunludur",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Geçersiz girdi",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Geçersiz alan adı",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Bu örnek, <code>{{appUrl}}</code> adresinden erişilecek şekilde yapılandırılmıştır, ancak <code>{{currentUrl}}</code> kullanılmaktadır. Devam ederseniz, kimlik doğrulama ile ilgili sorunlarla karşılaşabilirsiniz.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Yoksay",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Doğru alana gidin",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "З поверненням, увійдіть через",
|
"loginTitle": "З поверненням, увійдіть через",
|
||||||
"loginTitleSimple": "Welcome back, please login",
|
"loginTitleSimple": "З поверненням, будь ласка, авторизуйтесь",
|
||||||
"loginDivider": "Or",
|
"loginDivider": "Або",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Ім'я користувача",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Пароль",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Увійти",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Не вдалося авторизуватися",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Перевірте ім'я користувача та пароль",
|
||||||
"loginFailRateLimit": "You failed to login too many times. Please try again later",
|
"loginFailRateLimit": "Ви не змогли увійти занадто багато разів. Будь ласка, спробуйте ще раз пізніше",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Вхід здійснено",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "З поверненням!",
|
||||||
"loginOauthFailTitle": "An error occurred",
|
"loginOauthFailTitle": "Виникла помилка",
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
"loginOauthFailSubtitle": "Не вдалося отримати OAuth URL",
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
"loginOauthSuccessTitle": "Перенаправляємо",
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
"loginOauthSuccessSubtitle": "Перенаправляємо до вашого провайдера OAuth",
|
||||||
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
|
"loginOauthAutoRedirectTitle": "Автоматичне переспрямування OAuth",
|
||||||
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
|
"loginOauthAutoRedirectSubtitle": "Ви будете автоматично перенаправлені до вашого провайдера OAuth для автентифікації.",
|
||||||
"loginOauthAutoRedirectButton": "Redirect now",
|
"loginOauthAutoRedirectButton": "Перейти зараз",
|
||||||
"continueTitle": "Continue",
|
"continueTitle": "Продовжити",
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
"continueRedirectingTitle": "Перенаправлення...",
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
"continueRedirectingSubtitle": "Незабаром ви будете перенаправлені в додаток",
|
||||||
"continueRedirectManually": "Redirect me manually",
|
"continueRedirectManually": "Перенаправити мене вручну",
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
"continueInsecureRedirectTitle": "Небезпечне перенаправлення",
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
|
"continueInsecureRedirectSubtitle": "Ви намагаєтесь перенаправити з <code>https</code> на <code>http</code> який не є безпечним. Ви впевнені, що хочете продовжити?",
|
||||||
"continueUntrustedRedirectTitle": "Untrusted redirect",
|
"continueUntrustedRedirectTitle": "Недовірене перенаправлення",
|
||||||
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
|
"continueUntrustedRedirectSubtitle": "Ви намагаєтесь перенаправити на домен, який не збігається з вашим налаштованим доменом (<code>{{cookieDomain}}</code>). Впевнені, що хочете продовжити?",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Не вдалося вийти",
|
||||||
"logoutFailSubtitle": "Please try again",
|
"logoutFailSubtitle": "Будь ласка, спробуйте знову",
|
||||||
"logoutSuccessTitle": "Logged out",
|
"logoutSuccessTitle": "Ви вийшли",
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
"logoutSuccessSubtitle": "Ви вийшли з системи",
|
||||||
"logoutTitle": "Logout",
|
"logoutTitle": "Вийти",
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
|
"logoutUsernameSubtitle": "Зараз ви увійшли як <code>{{username}}</code>. Натисніть кнопку нижче для виходу.",
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
|
"logoutOauthSubtitle": "Наразі ви увійшли як <code>{{username}}</code> використовуючи провайдера {{provider}} OAuth. Натисніть кнопку нижче, щоб вийти.",
|
||||||
"notFoundTitle": "Page not found",
|
"notFoundTitle": "Сторінку не знайдено",
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
"notFoundSubtitle": "Сторінка, яку ви шукаєте, не існує.",
|
||||||
"notFoundButton": "Go home",
|
"notFoundButton": "На головну",
|
||||||
"totpFailTitle": "Failed to verify code",
|
"totpFailTitle": "Не вдалося перевірити код",
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
"totpFailSubtitle": "Перевірте ваш код і спробуйте ще раз",
|
||||||
"totpSuccessTitle": "Verified",
|
"totpSuccessTitle": "Перевірено",
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Перенаправлення до вашого додатку",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Введіть ваш TOTP код",
|
||||||
"totpSubtitle": "Please enter the code from your authenticator app.",
|
"totpSubtitle": "Будь ласка, введіть код з вашого додатку для автентифікації.",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Доступ обмежено",
|
||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "Користувач з ім'ям користувача <code>{{username}}</code> не має права доступу до ресурсу <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "Користувач з іменем <code>{{username}}</code> не авторизований для входу.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "Користувач з іменем <code>{{username}}</code> не входить до груп, що необхідні для ресурсу <code>{{resource}}</code>.",
|
||||||
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedIpSubtitle": "Ваша IP-адреса <code>{{ip}}</code> не авторизована для доступу до ресурсу <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Спробуйте ще раз",
|
||||||
"cancelTitle": "Cancel",
|
"cancelTitle": "Скасовувати",
|
||||||
"forgotPasswordTitle": "Forgot your password?",
|
"forgotPasswordTitle": "Забули пароль?",
|
||||||
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
|
"failedToFetchProvidersTitle": "Не вдалося завантажити провайдерів автентифікації. Будь ласка, перевірте вашу конфігурацію.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "Виникла помилка",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "Ви можете скинути пароль, змінивши змінну середовища \"USERS\".",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "Це поле обов'язкове для заповнення",
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Невірне введення",
|
||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Невірний домен",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "Даний ресурс налаштований для доступу з <code>{{appUrl}}</code>, але використовується <code>{{currentUrl}}</code>. Якщо ви продовжите, можуть виникнути проблеми з автентифікацією.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ігнорувати",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Перейти за коректним доменом",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "Bạn quên mật khẩu?",
|
"forgotPasswordTitle": "Bạn quên mật khẩu?",
|
||||||
"failedToFetchProvidersTitle": "Không tải được nhà cung cấp xác thực. Vui lòng kiểm tra cấu hình của bạn.",
|
"failedToFetchProvidersTitle": "Không tải được nhà cung cấp xác thực. Vui lòng kiểm tra cấu hình của bạn.",
|
||||||
"errorTitle": "An error occurred",
|
"errorTitle": "An error occurred",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "Đã xảy ra lỗi khi thực hiện thao tác này. Vui lòng kiểm tra bảng điều khiển để biết thêm thông tin.",
|
"errorSubtitle": "Đã xảy ra lỗi khi thực hiện thao tác này. Vui lòng kiểm tra bảng điều khiển để biết thêm thông tin.",
|
||||||
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
|
||||||
"fieldRequired": "This field is required",
|
"fieldRequired": "This field is required",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "Invalid Domain",
|
"domainWarningTitle": "Invalid Domain",
|
||||||
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
|
||||||
"ignoreTitle": "Ignore",
|
"ignoreTitle": "Ignore",
|
||||||
"goToCorrectDomainTitle": "Go to correct domain"
|
"goToCorrectDomainTitle": "Go to correct domain",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "忘记密码?",
|
"forgotPasswordTitle": "忘记密码?",
|
||||||
"failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。",
|
"failedToFetchProvidersTitle": "加载身份验证提供程序失败,请检查您的配置。",
|
||||||
"errorTitle": "发生了错误",
|
"errorTitle": "发生了错误",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
|
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
|
||||||
"forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。",
|
"forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。",
|
||||||
"fieldRequired": "必添字段",
|
"fieldRequired": "必添字段",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "无效域名",
|
"domainWarningTitle": "无效域名",
|
||||||
"domainWarningSubtitle": "当前实例配置的访问地址为 <code>{{appUrl}}</code>,但您正在使用 <code>{{currentUrl}}</code>。若继续操作,可能会遇到身份验证问题。",
|
"domainWarningSubtitle": "当前实例配置的访问地址为 <code>{{appUrl}}</code>,但您正在使用 <code>{{currentUrl}}</code>。若继续操作,可能会遇到身份验证问题。",
|
||||||
"ignoreTitle": "忽略",
|
"ignoreTitle": "忽略",
|
||||||
"goToCorrectDomainTitle": "转到正确的域名"
|
"goToCorrectDomainTitle": "转到正确的域名",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"forgotPasswordTitle": "忘記密碼?",
|
"forgotPasswordTitle": "忘記密碼?",
|
||||||
"failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。",
|
"failedToFetchProvidersTitle": "載入驗證供應商失敗。請檢查您的設定。",
|
||||||
"errorTitle": "發生錯誤",
|
"errorTitle": "發生錯誤",
|
||||||
|
"errorSubtitleInfo": "The following error occurred while processing your request:",
|
||||||
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。",
|
"errorSubtitle": "執行此操作時發生錯誤。請檢查主控台以獲取更多資訊。",
|
||||||
"forgotPasswordMessage": "透過修改 `USERS` 環境變數,你可以重設你的密碼。",
|
"forgotPasswordMessage": "透過修改 `USERS` 環境變數,你可以重設你的密碼。",
|
||||||
"fieldRequired": "此為必填欄位",
|
"fieldRequired": "此為必填欄位",
|
||||||
@@ -58,5 +59,23 @@
|
|||||||
"domainWarningTitle": "無效的網域",
|
"domainWarningTitle": "無效的網域",
|
||||||
"domainWarningSubtitle": "此服務設定為透過 <code>{{appUrl}}</code> 存取,但目前使用的是 <code>{{currentUrl}}</code>。若繼續操作,可能會遇到驗證問題。",
|
"domainWarningSubtitle": "此服務設定為透過 <code>{{appUrl}}</code> 存取,但目前使用的是 <code>{{currentUrl}}</code>。若繼續操作,可能會遇到驗證問題。",
|
||||||
"ignoreTitle": "忽略",
|
"ignoreTitle": "忽略",
|
||||||
"goToCorrectDomainTitle": "前往正確域名"
|
"goToCorrectDomainTitle": "前往正確域名",
|
||||||
}
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCardTitle": "Continue to {{app}}?",
|
||||||
|
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
|
||||||
|
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
|
||||||
|
"authorizeLoadingTitle": "Loading...",
|
||||||
|
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
|
||||||
|
"authorizeSuccessTitle": "Authorized",
|
||||||
|
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
|
||||||
|
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
|
||||||
|
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
|
||||||
|
"openidScopeName": "OpenID Connect",
|
||||||
|
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
|
||||||
|
"emailScopeName": "Email",
|
||||||
|
"emailScopeDescription": "Allows the app to access your email address.",
|
||||||
|
"profileScopeName": "Profile",
|
||||||
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
|
"groupsScopeName": "Groups",
|
||||||
|
"groupsScopeDescription": "Allows the app to access your group information."
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,15 +5,6 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isValidUrl = (url: string) => {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const capitalize = (str: string) => {
|
export const capitalize = (str: string) => {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { AppContextProvider } from "./context/app-context.tsx";
|
|||||||
import { UserContextProvider } from "./context/user-context.tsx";
|
import { UserContextProvider } from "./context/user-context.tsx";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
|
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
|
||||||
|
import { AuthorizePage } from "./pages/authorize-page.tsx";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<Route element={<Layout />} errorElement={<ErrorPage />}>
|
<Route element={<Layout />} errorElement={<ErrorPage />}>
|
||||||
<Route path="/" element={<App />} />
|
<Route path="/" element={<App />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/authorize" element={<AuthorizePage />} />
|
||||||
<Route path="/logout" element={<LogoutPage />} />
|
<Route path="/logout" element={<LogoutPage />} />
|
||||||
<Route path="/continue" element={<ContinuePage />} />
|
<Route path="/continue" element={<ContinuePage />} />
|
||||||
<Route path="/totp" element={<TotpPage />} />
|
<Route path="/totp" element={<TotpPage />} />
|
||||||
|
|||||||
199
frontend/src/pages/authorize-page.tsx
Normal file
199
frontend/src/pages/authorize-page.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { useUserContext } from "@/context/user-context";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { Navigate, useNavigate } from "react-router";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { getOidcClientInfoSchema } from "@/schemas/oidc-schemas";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
import { Mail, Shield, User, Users } from "lucide-react";
|
||||||
|
|
||||||
|
type Scope = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopeMapIconProps = {
|
||||||
|
className: "stroke-card stroke-2.5",
|
||||||
|
};
|
||||||
|
|
||||||
|
const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "openid",
|
||||||
|
name: t("openidScopeName"),
|
||||||
|
description: t("openidScopeDescription"),
|
||||||
|
icon: <Shield {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "email",
|
||||||
|
name: t("emailScopeName"),
|
||||||
|
description: t("emailScopeDescription"),
|
||||||
|
icon: <Mail {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "profile",
|
||||||
|
name: t("profileScopeName"),
|
||||||
|
description: t("profileScopeDescription"),
|
||||||
|
icon: <User {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "groups",
|
||||||
|
name: t("groupsScopeName"),
|
||||||
|
description: t("groupsScopeDescription"),
|
||||||
|
icon: <Users {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthorizePage = () => {
|
||||||
|
const { isLoggedIn } = useUserContext();
|
||||||
|
const { search } = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const scopeMap = createScopeMap(t);
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
const {
|
||||||
|
values: props,
|
||||||
|
missingParams,
|
||||||
|
isOidc,
|
||||||
|
compiled: compiledOIDCParams,
|
||||||
|
} = useOIDCParams(searchParams);
|
||||||
|
const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
|
||||||
|
|
||||||
|
const getClientInfo = useQuery({
|
||||||
|
queryKey: ["client", props.client_id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
|
||||||
|
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: isOidc,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizeMutation = useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
return axios.post("/api/oidc/authorize", {
|
||||||
|
scope: props.scope,
|
||||||
|
response_type: props.response_type,
|
||||||
|
client_id: props.client_id,
|
||||||
|
redirect_uri: props.redirect_uri,
|
||||||
|
state: props.state,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mutationKey: ["authorize", props.client_id],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.info(t("authorizeSuccessTitle"), {
|
||||||
|
description: t("authorizeSuccessSubtitle"),
|
||||||
|
});
|
||||||
|
window.location.replace(data.data.redirect_uri);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
window.location.replace(
|
||||||
|
`/error?error=${encodeURIComponent(error.message)}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingParams.length > 0) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getClientInfo.isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl">
|
||||||
|
{t("authorizeLoadingTitle")}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getClientInfo.isError) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={`/error?error=${encodeURIComponent(t("authorizeErrorClientInfo"))}`}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="min-w-xs sm:min-w-sm mx-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl">
|
||||||
|
{t("authorizeCardTitle", {
|
||||||
|
app: getClientInfo.data?.name || "Unknown",
|
||||||
|
})}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{scopes.includes("openid")
|
||||||
|
? t("authorizeSubtitle")
|
||||||
|
: t("authorizeSubtitleOAuth")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{scopes.includes("openid") && (
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{scopes.map((id) => {
|
||||||
|
const scope = scopeMap.find((s) => s.id === id);
|
||||||
|
if (!scope) return null;
|
||||||
|
return (
|
||||||
|
<div key={scope.id} className="flex flex-row items-center gap-3">
|
||||||
|
<div className="p-2 flex flex-col items-center justify-center bg-card-foreground rounded-md">
|
||||||
|
{scope.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<div className="text-md">{scope.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{scope.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => authorizeMutation.mutate()}
|
||||||
|
loading={authorizeMutation.isPending}
|
||||||
|
>
|
||||||
|
{t("authorizeTitle")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
disabled={authorizeMutation.isPending}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("cancelTitle")}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { useAppContext } from "@/context/app-context";
|
import { useAppContext } from "@/context/app-context";
|
||||||
import { useUserContext } from "@/context/user-context";
|
import { useUserContext } from "@/context/user-context";
|
||||||
import { isValidUrl } from "@/lib/utils";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Navigate, useLocation, useNavigate } from "react-router";
|
import { Navigate, useLocation, useNavigate } from "react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
|
||||||
|
|
||||||
export const ContinuePage = () => {
|
export const ContinuePage = () => {
|
||||||
const { cookieDomain, disableUiWarnings } = useAppContext();
|
const { cookieDomain, disableUiWarnings } = useAppContext();
|
||||||
@@ -20,59 +20,55 @@ export const ContinuePage = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||||
|
const hasRedirected = useRef(false);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const redirectUri = searchParams.get("redirect_uri");
|
const redirectUri = searchParams.get("redirect_uri");
|
||||||
|
|
||||||
const isValidRedirectUri =
|
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
||||||
redirectUri !== null ? isValidUrl(redirectUri) : false;
|
redirectUri,
|
||||||
const redirectUriObj = isValidRedirectUri
|
cookieDomain,
|
||||||
? new URL(redirectUri as string)
|
);
|
||||||
: null;
|
|
||||||
const isTrustedRedirectUri =
|
|
||||||
redirectUriObj !== null
|
|
||||||
? redirectUriObj.hostname === cookieDomain ||
|
|
||||||
redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
|
|
||||||
: false;
|
|
||||||
const isAllowedRedirectProto =
|
|
||||||
redirectUriObj !== null
|
|
||||||
? redirectUriObj.protocol === "https:" ||
|
|
||||||
redirectUriObj.protocol === "http:"
|
|
||||||
: false;
|
|
||||||
const isHttpsDowngrade =
|
|
||||||
redirectUriObj !== null
|
|
||||||
? redirectUriObj.protocol === "http:" &&
|
|
||||||
window.location.protocol === "https:"
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const handleRedirect = () => {
|
const urlHref = url?.href;
|
||||||
setLoading(true);
|
|
||||||
window.location.assign(redirectUriObj!.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
const hasValidRedirect = valid && allowedProto;
|
||||||
if (!isLoggedIn) {
|
const showUntrustedWarning =
|
||||||
|
hasValidRedirect && !trusted && !disableUiWarnings;
|
||||||
|
const showInsecureWarning =
|
||||||
|
hasValidRedirect && httpsDowngrade && !disableUiWarnings;
|
||||||
|
const shouldAutoRedirect =
|
||||||
|
isLoggedIn &&
|
||||||
|
hasValidRedirect &&
|
||||||
|
!showUntrustedWarning &&
|
||||||
|
!showInsecureWarning;
|
||||||
|
|
||||||
|
const redirectToTarget = useCallback(() => {
|
||||||
|
if (!urlHref || hasRedirected.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
hasRedirected.current = true;
|
||||||
(!isValidRedirectUri ||
|
window.location.assign(urlHref);
|
||||||
!isAllowedRedirectProto ||
|
}, [urlHref]);
|
||||||
!isTrustedRedirectUri ||
|
|
||||||
isHttpsDowngrade) &&
|
const handleRedirect = useCallback(() => {
|
||||||
!disableUiWarnings
|
setIsLoading(true);
|
||||||
) {
|
redirectToTarget();
|
||||||
|
}, [redirectToTarget]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldAutoRedirect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto = setTimeout(() => {
|
const auto = setTimeout(() => {
|
||||||
handleRedirect();
|
redirectToTarget();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const reveal = setTimeout(() => {
|
const reveal = setTimeout(() => {
|
||||||
setLoading(false);
|
|
||||||
setShowRedirectButton(true);
|
setShowRedirectButton(true);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
@@ -80,22 +76,22 @@ export const ContinuePage = () => {
|
|||||||
clearTimeout(auto);
|
clearTimeout(auto);
|
||||||
clearTimeout(reveal);
|
clearTimeout(reveal);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [shouldAutoRedirect, redirectToTarget]);
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`}
|
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||||
replace
|
replace
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidRedirectUri || !isAllowedRedirectProto) {
|
if (!hasValidRedirect) {
|
||||||
return <Navigate to="/logout" replace />;
|
return <Navigate to="/logout" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTrustedRedirectUri && !disableUiWarnings) {
|
if (showUntrustedWarning) {
|
||||||
return (
|
return (
|
||||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -116,7 +112,7 @@ export const ContinuePage = () => {
|
|||||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRedirect}
|
onClick={handleRedirect}
|
||||||
loading={loading}
|
loading={isLoading}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
{t("continueTitle")}
|
{t("continueTitle")}
|
||||||
@@ -124,7 +120,7 @@ export const ContinuePage = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/logout")}
|
onClick={() => navigate("/logout")}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={loading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t("cancelTitle")}
|
{t("cancelTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -133,7 +129,7 @@ export const ContinuePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHttpsDowngrade && !disableUiWarnings) {
|
if (showInsecureWarning) {
|
||||||
return (
|
return (
|
||||||
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -151,13 +147,17 @@ export const ContinuePage = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||||
<Button onClick={handleRedirect} loading={loading} variant="warning">
|
<Button
|
||||||
|
onClick={handleRedirect}
|
||||||
|
loading={isLoading}
|
||||||
|
variant="warning"
|
||||||
|
>
|
||||||
{t("continueTitle")}
|
{t("continueTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/logout")}
|
onClick={() => navigate("/logout")}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={loading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t("cancelTitle")}
|
{t("cancelTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,15 +5,30 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
export const ErrorPage = () => {
|
export const ErrorPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { search } = useLocation();
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
const error = searchParams.get("error") ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="min-w-xs sm:min-w-sm">
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
|
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
|
||||||
<CardDescription>{t("errorSubtitle")}</CardDescription>
|
<CardDescription className="flex flex-col gap-1.5">
|
||||||
|
{error ? (
|
||||||
|
<>
|
||||||
|
<p>{t("errorSubtitleInfo")}</p>
|
||||||
|
<pre>{error}</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>{t("errorSubtitle")}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { OAuthButton } from "@/components/ui/oauth-button";
|
|||||||
import { SeperatorWithChildren } from "@/components/ui/separator";
|
import { SeperatorWithChildren } from "@/components/ui/separator";
|
||||||
import { useAppContext } from "@/context/app-context";
|
import { useAppContext } from "@/context/app-context";
|
||||||
import { useUserContext } from "@/context/user-context";
|
import { useUserContext } from "@/context/user-context";
|
||||||
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
import { LoginSchema } from "@/schemas/login-schema";
|
import { LoginSchema } from "@/schemas/login-schema";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
@@ -39,26 +40,43 @@ export const LoginPage = () => {
|
|||||||
const { providers, title, oauthAutoRedirect } = useAppContext();
|
const { providers, title, oauthAutoRedirect } = useAppContext();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] =
|
|
||||||
useState(false);
|
|
||||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||||
|
|
||||||
|
const hasAutoRedirectedRef = useRef(false);
|
||||||
|
|
||||||
const redirectTimer = useRef<number | null>(null);
|
const redirectTimer = useRef<number | null>(null);
|
||||||
const redirectButtonTimer = useRef<number | null>(null);
|
const redirectButtonTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const redirectUri = searchParams.get("redirect_uri");
|
const {
|
||||||
|
values: props,
|
||||||
|
isOidc,
|
||||||
|
compiled: compiledOIDCParams,
|
||||||
|
} = useOIDCParams(searchParams);
|
||||||
|
|
||||||
|
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
||||||
|
providers.find((provider) => provider.id === oauthAutoRedirect) !==
|
||||||
|
undefined && props.redirect_uri,
|
||||||
|
);
|
||||||
|
|
||||||
const oauthProviders = providers.filter(
|
const oauthProviders = providers.filter(
|
||||||
(provider) => provider.id !== "username",
|
(provider) => provider.id !== "local" && provider.id !== "ldap",
|
||||||
);
|
);
|
||||||
const userAuthConfigured =
|
const userAuthConfigured =
|
||||||
providers.find((provider) => provider.id === "username") !== undefined;
|
providers.find(
|
||||||
|
(provider) => provider.id === "local" || provider.id === "ldap",
|
||||||
|
) !== undefined;
|
||||||
|
|
||||||
const oauthMutation = useMutation({
|
const {
|
||||||
|
mutate: oauthMutate,
|
||||||
|
data: oauthData,
|
||||||
|
isPending: oauthIsPending,
|
||||||
|
variables: oauthVariables,
|
||||||
|
} = useMutation({
|
||||||
mutationFn: (provider: string) =>
|
mutationFn: (provider: string) =>
|
||||||
axios.get(
|
axios.get(
|
||||||
`/api/oauth/url/${provider}?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
),
|
),
|
||||||
mutationKey: ["oauth"],
|
mutationKey: ["oauth"],
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -69,22 +87,28 @@ export const LoginPage = () => {
|
|||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
window.location.replace(data.data.url);
|
window.location.replace(data.data.url);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
if (isOauthAutoRedirect) {
|
||||||
|
redirectButtonTimer.current = window.setTimeout(() => {
|
||||||
|
setShowRedirectButton(true);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setOauthAutoRedirectHandover(false);
|
setIsOauthAutoRedirect(false);
|
||||||
toast.error(t("loginOauthFailTitle"), {
|
toast.error(t("loginOauthFailTitle"), {
|
||||||
description: t("loginOauthFailSubtitle"),
|
description: t("loginOauthFailSubtitle"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const { mutate: loginMutate, isPending: loginIsPending } = useMutation({
|
||||||
mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values),
|
mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values),
|
||||||
mutationKey: ["login"],
|
mutationKey: ["login"],
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.data.totpPending) {
|
if (data.data.totpPending) {
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
`/totp?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,8 +118,12 @@ export const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
|
if (isOidc) {
|
||||||
|
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
@@ -111,33 +139,43 @@ export const LoginPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
providers.find((provider) => provider.id === oauthAutoRedirect) &&
|
|
||||||
!isLoggedIn &&
|
!isLoggedIn &&
|
||||||
redirectUri
|
isOauthAutoRedirect &&
|
||||||
|
!hasAutoRedirectedRef.current &&
|
||||||
|
props.redirect_uri
|
||||||
) {
|
) {
|
||||||
// Not sure of a better way to do this
|
hasAutoRedirectedRef.current = true;
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
oauthMutate(oauthAutoRedirect);
|
||||||
setOauthAutoRedirectHandover(true);
|
|
||||||
oauthMutation.mutate(oauthAutoRedirect);
|
|
||||||
redirectButtonTimer.current = window.setTimeout(() => {
|
|
||||||
setShowRedirectButton(true);
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, [
|
||||||
|
isLoggedIn,
|
||||||
|
oauthMutate,
|
||||||
|
hasAutoRedirectedRef,
|
||||||
|
oauthAutoRedirect,
|
||||||
|
isOauthAutoRedirect,
|
||||||
|
props.redirect_uri,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
return () => {
|
||||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
if (redirectTimer.current) {
|
||||||
if (redirectButtonTimer.current)
|
clearTimeout(redirectTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectButtonTimer.current) {
|
||||||
clearTimeout(redirectButtonTimer.current);
|
clearTimeout(redirectButtonTimer.current);
|
||||||
},
|
}
|
||||||
[],
|
};
|
||||||
);
|
}, [redirectTimer, redirectButtonTimer]);
|
||||||
|
|
||||||
if (isLoggedIn && redirectUri) {
|
if (isLoggedIn && isOidc) {
|
||||||
|
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn && props.redirect_uri !== "") {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
|
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`}
|
||||||
replace
|
replace
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -147,7 +185,7 @@ export const LoginPage = () => {
|
|||||||
return <Navigate to="/logout" replace />;
|
return <Navigate to="/logout" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oauthAutoRedirectHandover) {
|
if (isOauthAutoRedirect) {
|
||||||
return (
|
return (
|
||||||
<Card className="min-w-xs sm:min-w-sm">
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -162,7 +200,14 @@ export const LoginPage = () => {
|
|||||||
<CardFooter className="flex flex-col items-stretch">
|
<CardFooter className="flex flex-col items-stretch">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.replace(oauthMutation.data?.data.url);
|
if (oauthData?.data.url) {
|
||||||
|
window.location.replace(oauthData.data.url);
|
||||||
|
} else {
|
||||||
|
setIsOauthAutoRedirect(false);
|
||||||
|
toast.error(t("loginOauthFailTitle"), {
|
||||||
|
description: t("loginOauthFailSubtitle"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("loginOauthAutoRedirectButton")}
|
{t("loginOauthAutoRedirectButton")}
|
||||||
@@ -193,12 +238,9 @@ export const LoginPage = () => {
|
|||||||
title={provider.name}
|
title={provider.name}
|
||||||
icon={iconMap[provider.id] ?? <OAuthIcon />}
|
icon={iconMap[provider.id] ?? <OAuthIcon />}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => oauthMutation.mutate(provider.id)}
|
onClick={() => oauthMutate(provider.id)}
|
||||||
loading={
|
loading={oauthIsPending && oauthVariables === provider.id}
|
||||||
oauthMutation.isPending &&
|
disabled={oauthIsPending || loginIsPending}
|
||||||
oauthMutation.variables === provider.id
|
|
||||||
}
|
|
||||||
disabled={oauthMutation.isPending || loginMutation.isPending}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,8 +250,8 @@ export const LoginPage = () => {
|
|||||||
)}
|
)}
|
||||||
{userAuthConfigured && (
|
{userAuthConfigured && (
|
||||||
<LoginForm
|
<LoginForm
|
||||||
onSubmit={(values) => loginMutation.mutate(values)}
|
onSubmit={(values) => loginMutate(values)}
|
||||||
loading={loginMutation.isPending || oauthMutation.isPending}
|
loading={loginIsPending || oauthIsPending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{providers.length == 0 && (
|
{providers.length == 0 && (
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const LogoutPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
window.location.assign("/login");
|
window.location.replace("/login");
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -39,12 +39,13 @@ export const LogoutPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
return () => {
|
||||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
if (redirectTimer.current) {
|
||||||
},
|
clearTimeout(redirectTimer.current);
|
||||||
[],
|
}
|
||||||
);
|
};
|
||||||
|
}, [redirectTimer]);
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
@@ -55,7 +56,7 @@ export const LogoutPage = () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
|
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{provider !== "username" ? (
|
{provider !== "local" && provider !== "ldap" ? (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="logoutOauthSubtitle"
|
i18nKey="logoutOauthSubtitle"
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useEffect, useId, useRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Navigate, useLocation } from "react-router";
|
import { Navigate, useLocation } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
|
|
||||||
export const TotpPage = () => {
|
export const TotpPage = () => {
|
||||||
const { totpPending } = useUserContext();
|
const { totpPending } = useUserContext();
|
||||||
@@ -26,7 +27,11 @@ export const TotpPage = () => {
|
|||||||
const redirectTimer = useRef<number | null>(null);
|
const redirectTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const redirectUri = searchParams.get("redirect_uri");
|
const {
|
||||||
|
values: props,
|
||||||
|
isOidc,
|
||||||
|
compiled: compiledOIDCParams,
|
||||||
|
} = useOIDCParams(searchParams);
|
||||||
|
|
||||||
const totpMutation = useMutation({
|
const totpMutation = useMutation({
|
||||||
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
||||||
@@ -37,8 +42,13 @@ export const TotpPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
redirectTimer.current = window.setTimeout(() => {
|
||||||
|
if (isOidc) {
|
||||||
|
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
@@ -49,12 +59,13 @@ export const TotpPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
return () => {
|
||||||
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
if (redirectTimer.current) {
|
||||||
},
|
clearTimeout(redirectTimer.current);
|
||||||
[],
|
}
|
||||||
);
|
};
|
||||||
|
}, [redirectTimer]);
|
||||||
|
|
||||||
if (!totpPending) {
|
if (!totpPending) {
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
@@ -70,7 +81,6 @@ export const TotpPage = () => {
|
|||||||
<TotpForm
|
<TotpForm
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSubmit={(values) => totpMutation.mutate(values)}
|
onSubmit={(values) => totpMutation.mutate(values)}
|
||||||
loading={totpMutation.isPending}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col items-stretch">
|
<CardFooter className="flex flex-col items-stretch">
|
||||||
|
|||||||
5
frontend/src/schemas/oidc-schemas.ts
Normal file
5
frontend/src/schemas/oidc-schemas.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const getOidcClientInfoSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
@@ -24,6 +24,11 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/resources/, ""),
|
rewrite: (path) => path.replace(/^\/resources/, ""),
|
||||||
},
|
},
|
||||||
|
"/.well-known": {
|
||||||
|
target: "http://tinyauth-backend:3000/.well-known",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/\.well-known/, ""),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/charmbracelet/huh v0.8.0
|
github.com/charmbracelet/huh v0.8.0
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/go-querystring v1.2.0
|
github.com/google/go-querystring v1.2.0
|
||||||
@@ -24,7 +25,7 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||||
golang.org/x/oauth2 v0.34.0
|
golang.org/x/oauth2 v0.34.0
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
modernc.org/sqlite v1.44.0
|
modernc.org/sqlite v1.44.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -119,7 +120,7 @@ require (
|
|||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.67.4 // indirect
|
modernc.org/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -103,6 +103,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
|||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -383,8 +385,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
|||||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||||
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -393,8 +395,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc=
|
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||||
modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE=
|
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
3
internal/assets/migrations/000005_oidc_session.down.sql
Normal file
3
internal/assets/migrations/000005_oidc_session.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS "oidc_tokens";
|
||||||
|
DROP TABLE IF EXISTS "oidc_userinfo";
|
||||||
|
DROP TABLE IF EXISTS "oidc_codes";
|
||||||
27
internal/assets/migrations/000005_oidc_session.up.sql
Normal file
27
internal/assets/migrations/000005_oidc_session.up.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "oidc_codes" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE,
|
||||||
|
"code_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"redirect_uri" TEXT NOT NULL,
|
||||||
|
"client_id" TEXT NOT NULL,
|
||||||
|
"expires_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE,
|
||||||
|
"access_token_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
"refresh_token_hash" TEXT NOT NULL,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"client_id" TEXT NOT NULL,
|
||||||
|
"token_expires_at" INTEGER NOT NULL,
|
||||||
|
"refresh_token_expires_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"preferred_username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"groups" TEXT NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config config.Config
|
config config.Config
|
||||||
context struct {
|
context struct {
|
||||||
|
appUrl string
|
||||||
uuid string
|
uuid string
|
||||||
cookieDomain string
|
cookieDomain string
|
||||||
sessionCookieName string
|
sessionCookieName string
|
||||||
@@ -30,6 +31,7 @@ type BootstrapApp struct {
|
|||||||
users []config.User
|
users []config.User
|
||||||
oauthProviders map[string]config.OAuthServiceConfig
|
oauthProviders map[string]config.OAuthServiceConfig
|
||||||
configuredProviders []controller.Provider
|
configuredProviders []controller.Provider
|
||||||
|
oidcClients []config.OIDCClientConfig
|
||||||
}
|
}
|
||||||
services Services
|
services Services
|
||||||
}
|
}
|
||||||
@@ -41,10 +43,20 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) Setup() error {
|
func (app *BootstrapApp) Setup() error {
|
||||||
|
// get app url
|
||||||
|
appUrl, err := url.Parse(app.config.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
app.context.appUrl = appUrl.Scheme + "://" + appUrl.Host
|
||||||
|
|
||||||
// validate session config
|
// validate session config
|
||||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||||
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse users
|
// Parse users
|
||||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
||||||
|
|
||||||
@@ -61,16 +73,12 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||||
provider.ClientSecret = secret
|
provider.ClientSecret = secret
|
||||||
provider.ClientSecretFile = ""
|
provider.ClientSecretFile = ""
|
||||||
app.context.oauthProviders[name] = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
for id := range config.OverrideProviders {
|
if provider.RedirectURL == "" {
|
||||||
if provider, exists := app.context.oauthProviders[id]; exists {
|
provider.RedirectURL = app.context.appUrl + "/api/oauth/callback/" + name
|
||||||
if provider.RedirectURL == "" {
|
|
||||||
provider.RedirectURL = app.config.AppURL + "/api/oauth/callback/" + id
|
|
||||||
app.context.oauthProviders[id] = provider
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.context.oauthProviders[name] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, provider := range app.context.oauthProviders {
|
for id, provider := range app.context.oauthProviders {
|
||||||
@@ -84,8 +92,14 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.oauthProviders[id] = provider
|
app.context.oauthProviders[id] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup OIDC clients
|
||||||
|
for id, client := range app.config.OIDC.Clients {
|
||||||
|
client.ID = id
|
||||||
|
app.context.oidcClients = append(app.context.oidcClients, client)
|
||||||
|
}
|
||||||
|
|
||||||
// Get cookie domain
|
// Get cookie domain
|
||||||
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
|
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -94,7 +108,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.cookieDomain = cookieDomain
|
app.context.cookieDomain = cookieDomain
|
||||||
|
|
||||||
// Cookie names
|
// Cookie names
|
||||||
appUrl, _ := url.Parse(app.config.AppURL) // Already validated
|
|
||||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||||
@@ -144,10 +157,18 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return configuredProviders[i].Name < configuredProviders[j].Name
|
return configuredProviders[i].Name < configuredProviders[j].Name
|
||||||
})
|
})
|
||||||
|
|
||||||
if services.authService.UserAuthConfigured() {
|
if services.authService.LocalAuthConfigured() {
|
||||||
configuredProviders = append(configuredProviders, controller.Provider{
|
configuredProviders = append(configuredProviders, controller.Provider{
|
||||||
Name: "Username",
|
Name: "Local",
|
||||||
ID: "username",
|
ID: "local",
|
||||||
|
OAuth: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if services.authService.LdapAuthConfigured() {
|
||||||
|
configuredProviders = append(configuredProviders, controller.Provider{
|
||||||
|
Name: "LDAP",
|
||||||
|
ID: "ldap",
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -232,7 +253,7 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
|
|
||||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||||
|
|
||||||
for ; true; <-ticker.C {
|
for range ticker.C {
|
||||||
tlog.App.Debug().Msg("Sending heartbeat")
|
tlog.App.Debug().Msg("Sending heartbeat")
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
||||||
@@ -264,7 +285,7 @@ func (app *BootstrapApp) dbCleanup(queries *repository.Queries) {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for ; true; <-ticker.C {
|
for range ticker.C {
|
||||||
tlog.App.Debug().Msg("Cleaning up old database sessions")
|
tlog.App.Debug().Msg("Cleaning up old database sessions")
|
||||||
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
|
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/middleware"
|
"github.com/steveiliop56/tinyauth/internal/middleware"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var DEV_MODES = []string{"main", "test", "development"}
|
||||||
|
|
||||||
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||||
|
if !slices.Contains(DEV_MODES, config.Version) {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(gin.Recovery())
|
engine.Use(gin.Recovery())
|
||||||
|
|
||||||
@@ -78,6 +86,10 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
oauthController.SetupRoutes()
|
oauthController.SetupRoutes()
|
||||||
|
|
||||||
|
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter)
|
||||||
|
|
||||||
|
oidcController.SetupRoutes()
|
||||||
|
|
||||||
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||||
AppURL: app.config.AppURL,
|
AppURL: app.config.AppURL,
|
||||||
}, apiRouter, app.services.accessControlService, app.services.authService)
|
}, apiRouter, app.services.accessControlService, app.services.authService)
|
||||||
@@ -101,5 +113,9 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
healthController.SetupRoutes()
|
healthController.SetupRoutes()
|
||||||
|
|
||||||
|
wellknownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{}, app.services.oidcService, engine)
|
||||||
|
|
||||||
|
wellknownController.SetupRoutes()
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Services struct {
|
|||||||
dockerService *service.DockerService
|
dockerService *service.DockerService
|
||||||
ldapService *service.LdapService
|
ldapService *service.LdapService
|
||||||
oauthBrokerService *service.OAuthBrokerService
|
oauthBrokerService *service.OAuthBrokerService
|
||||||
|
oidcService *service.OIDCService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) {
|
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) {
|
||||||
@@ -30,12 +31,13 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
|
|
||||||
err := ldapService.Init()
|
err := ldapService.Init()
|
||||||
|
|
||||||
if err == nil {
|
if err != nil {
|
||||||
services.ldapService = ldapService
|
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
|
||||||
} else {
|
ldapService.Unconfigure()
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services.ldapService = ldapService
|
||||||
|
|
||||||
dockerService := service.NewDockerService()
|
dockerService := service.NewDockerService()
|
||||||
|
|
||||||
err = dockerService.Init()
|
err = dockerService.Init()
|
||||||
@@ -67,6 +69,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||||
SessionCookieName: app.context.sessionCookieName,
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
IP: app.config.Auth.IP,
|
IP: app.config.Auth.IP,
|
||||||
|
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
|
||||||
}, dockerService, services.ldapService, queries)
|
}, dockerService, services.ldapService, queries)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
@@ -87,5 +90,21 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
|
|
||||||
services.oauthBrokerService = oauthBrokerService
|
services.oauthBrokerService = oauthBrokerService
|
||||||
|
|
||||||
|
oidcService := service.NewOIDCService(service.OIDCServiceConfig{
|
||||||
|
Clients: app.config.OIDC.Clients,
|
||||||
|
PrivateKeyPath: app.config.OIDC.PrivateKeyPath,
|
||||||
|
PublicKeyPath: app.config.OIDC.PublicKeyPath,
|
||||||
|
Issuer: app.config.AppURL,
|
||||||
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
|
}, queries)
|
||||||
|
|
||||||
|
err = oidcService.Init()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Services{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
services.oidcService = oidcService
|
||||||
|
|
||||||
return services, nil
|
return services, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Config struct {
|
|||||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||||
|
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
@@ -60,6 +61,12 @@ type OAuthConfig struct {
|
|||||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OIDCConfig struct {
|
||||||
|
PrivateKeyPath string `description:"Path to the private key file." yaml:"privateKeyPath"`
|
||||||
|
PublicKeyPath string `description:"Path to the public key file." yaml:"publicKeyPath"`
|
||||||
|
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
|
||||||
|
}
|
||||||
|
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
Title string `description:"The title of the UI." yaml:"title"`
|
Title string `description:"The title of the UI." yaml:"title"`
|
||||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
||||||
@@ -67,14 +74,15 @@ type UIConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LdapConfig struct {
|
type LdapConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
||||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
||||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
||||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
||||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
||||||
|
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
@@ -113,16 +121,25 @@ type Claims struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID."`
|
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
||||||
ClientSecret string `description:"OAuth client secret."`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
||||||
ClientSecretFile string `description:"Path to the file containing the OAuth client secret."`
|
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"`
|
||||||
Scopes []string `description:"OAuth scopes."`
|
Scopes []string `description:"OAuth scopes." yaml:"scopes"`
|
||||||
RedirectURL string `description:"OAuth redirect URL."`
|
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"`
|
||||||
AuthURL string `description:"OAuth authorization URL."`
|
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"`
|
||||||
TokenURL string `description:"OAuth token URL."`
|
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"`
|
||||||
UserinfoURL string `description:"OAuth userinfo URL."`
|
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"`
|
||||||
Insecure bool `description:"Allow insecure OAuth connections."`
|
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"`
|
||||||
Name string `description:"Provider name in UI."`
|
Name string `description:"Provider name in UI." yaml:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCClientConfig struct {
|
||||||
|
ID string `description:"OIDC client ID." yaml:"-"`
|
||||||
|
ClientID string `description:"OIDC client ID." yaml:"clientId"`
|
||||||
|
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
|
||||||
|
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
|
||||||
|
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"`
|
||||||
|
Name string `description:"Client name in UI." yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var OverrideProviders = map[string]string{
|
var OverrideProviders = map[string]string{
|
||||||
@@ -138,28 +155,22 @@ type User struct {
|
|||||||
TotpSecret string
|
TotpSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LdapUser struct {
|
||||||
|
DN string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
type UserSearch struct {
|
type UserSearch struct {
|
||||||
Username string
|
Username string
|
||||||
Type string // local, ldap or unknown
|
Type string // local, ldap or unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionCookie struct {
|
|
||||||
UUID string
|
|
||||||
Username string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
Username string
|
Username string
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
IsLoggedIn bool
|
IsLoggedIn bool
|
||||||
|
IsBasicAuth bool
|
||||||
OAuth bool
|
OAuth bool
|
||||||
Provider string
|
Provider string
|
||||||
TotpPending bool
|
TotpPending bool
|
||||||
@@ -167,6 +178,7 @@ type UserContext struct {
|
|||||||
TotpEnabled bool
|
TotpEnabled bool
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
}
|
}
|
||||||
|
|
||||||
// API responses and queries
|
// API responses and queries
|
||||||
@@ -195,6 +207,7 @@ type App struct {
|
|||||||
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
||||||
Response AppResponse `description:"Response customization." yaml:"response"`
|
Response AppResponse `description:"Response customization." yaml:"response"`
|
||||||
Path AppPath `description:"Path access configuration." yaml:"path"`
|
Path AppPath `description:"Path access configuration." yaml:"path"`
|
||||||
|
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
@@ -211,6 +224,10 @@ type AppOAuth struct {
|
|||||||
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppLDAP struct {
|
||||||
|
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppIP struct {
|
type AppIP struct {
|
||||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ type UserContextResponse struct {
|
|||||||
OAuth bool `json:"oauth"`
|
OAuth bool `json:"oauth"`
|
||||||
TotpPending bool `json:"totpPending"`
|
TotpPending bool `json:"totpPending"`
|
||||||
OAuthName string `json:"oauthName"`
|
OAuthName string `json:"oauthName"`
|
||||||
OAuthSub string `json:"oauthSub"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppContextResponse struct {
|
type AppContextResponse struct {
|
||||||
@@ -90,7 +89,6 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
OAuth: context.OAuth,
|
OAuth: context.OAuth,
|
||||||
TotpPending: context.TotpPending,
|
TotpPending: context.TotpPending,
|
||||||
OAuthName: context.OAuthName,
|
OAuthName: context.OAuthName,
|
||||||
OAuthSub: context.OAuthSub,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var controllerCfg = controller.ContextControllerConfig{
|
var contextControllerCfg = controller.ContextControllerConfig{
|
||||||
Providers: []controller.Provider{
|
Providers: []controller.Provider{
|
||||||
{
|
{
|
||||||
Name: "Username",
|
Name: "Local",
|
||||||
ID: "username",
|
ID: "local",
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -35,13 +35,14 @@ var controllerCfg = controller.ContextControllerConfig{
|
|||||||
DisableUIWarnings: false,
|
DisableUIWarnings: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var userContext = config.UserContext{
|
var contextCtrlTestContext = config.UserContext{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: false,
|
TotpEnabled: false,
|
||||||
@@ -64,7 +65,7 @@ func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httpt
|
|||||||
|
|
||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
|
|
||||||
ctrl := controller.NewContextController(controllerCfg, group)
|
ctrl := controller.NewContextController(contextControllerCfg, group)
|
||||||
ctrl.SetupRoutes()
|
ctrl.SetupRoutes()
|
||||||
|
|
||||||
return router, recorder
|
return router, recorder
|
||||||
@@ -74,14 +75,14 @@ func TestAppContextHandler(t *testing.T) {
|
|||||||
expectedRes := controller.AppContextResponse{
|
expectedRes := controller.AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Providers: controllerCfg.Providers,
|
Providers: contextControllerCfg.Providers,
|
||||||
Title: controllerCfg.Title,
|
Title: contextControllerCfg.Title,
|
||||||
AppURL: controllerCfg.AppURL,
|
AppURL: contextControllerCfg.AppURL,
|
||||||
CookieDomain: controllerCfg.CookieDomain,
|
CookieDomain: contextControllerCfg.CookieDomain,
|
||||||
ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage,
|
ForgotPasswordMessage: contextControllerCfg.ForgotPasswordMessage,
|
||||||
BackgroundImage: controllerCfg.BackgroundImage,
|
BackgroundImage: contextControllerCfg.BackgroundImage,
|
||||||
OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect,
|
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
|
||||||
DisableUIWarnings: controllerCfg.DisableUIWarnings,
|
DisableUIWarnings: contextControllerCfg.DisableUIWarnings,
|
||||||
}
|
}
|
||||||
|
|
||||||
router, recorder := setupContextController(nil)
|
router, recorder := setupContextController(nil)
|
||||||
@@ -102,20 +103,20 @@ func TestUserContextHandler(t *testing.T) {
|
|||||||
expectedRes := controller.UserContextResponse{
|
expectedRes := controller.UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
IsLoggedIn: userContext.IsLoggedIn,
|
IsLoggedIn: contextCtrlTestContext.IsLoggedIn,
|
||||||
Username: userContext.Username,
|
Username: contextCtrlTestContext.Username,
|
||||||
Name: userContext.Name,
|
Name: contextCtrlTestContext.Name,
|
||||||
Email: userContext.Email,
|
Email: contextCtrlTestContext.Email,
|
||||||
Provider: userContext.Provider,
|
Provider: contextCtrlTestContext.Provider,
|
||||||
OAuth: userContext.OAuth,
|
OAuth: contextCtrlTestContext.OAuth,
|
||||||
TotpPending: userContext.TotpPending,
|
TotpPending: contextCtrlTestContext.TotpPending,
|
||||||
OAuthName: userContext.OAuthName,
|
OAuthName: contextCtrlTestContext.OAuthName,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with context
|
// Test with context
|
||||||
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
||||||
func(c *gin.Context) {
|
func(c *gin.Context) {
|
||||||
c.Set("context", &userContext)
|
c.Set("context", &contextCtrlTestContext)
|
||||||
c.Next()
|
c.Next()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
@@ -188,10 +189,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
username = user.PreferredUsername
|
username = user.PreferredUsername
|
||||||
} else {
|
} else {
|
||||||
tlog.App.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
tlog.App.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||||
username = strings.Replace(user.Email, "@", "_", -1)
|
username = strings.Replace(user.Email, "@", "_", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: username,
|
Username: username,
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
|
|||||||
458
internal/controller/oidc_controller.go
Normal file
458
internal/controller/oidc_controller.go
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OIDCControllerConfig struct{}
|
||||||
|
|
||||||
|
type OIDCController struct {
|
||||||
|
config OIDCControllerConfig
|
||||||
|
router *gin.RouterGroup
|
||||||
|
oidc *service.OIDCService
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizeCallback struct {
|
||||||
|
Code string `url:"code"`
|
||||||
|
State string `url:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenRequest struct {
|
||||||
|
GrantType string `form:"grant_type" binding:"required" url:"grant_type"`
|
||||||
|
Code string `form:"code" url:"code"`
|
||||||
|
RedirectURI string `form:"redirect_uri" url:"redirect_uri"`
|
||||||
|
RefreshToken string `form:"refresh_token" url:"refresh_token"`
|
||||||
|
ClientSecret string `form:"client_secret" url:"client_secret"`
|
||||||
|
ClientID string `form:"client_id" url:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackError struct {
|
||||||
|
Error string `url:"error"`
|
||||||
|
ErrorDescription string `url:"error_description"`
|
||||||
|
State string `url:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorScreen struct {
|
||||||
|
Error string `url:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientRequest struct {
|
||||||
|
ClientID string `uri:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientCredentials struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCController(config OIDCControllerConfig, oidcService *service.OIDCService, router *gin.RouterGroup) *OIDCController {
|
||||||
|
return &OIDCController{
|
||||||
|
config: config,
|
||||||
|
oidc: oidcService,
|
||||||
|
router: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) SetupRoutes() {
|
||||||
|
oidcGroup := controller.router.Group("/oidc")
|
||||||
|
oidcGroup.GET("/clients/:id", controller.GetClientInfo)
|
||||||
|
oidcGroup.POST("/authorize", controller.Authorize)
|
||||||
|
oidcGroup.POST("/token", controller.Token)
|
||||||
|
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||||
|
var req ClientRequest
|
||||||
|
|
||||||
|
err := c.BindUri(&req)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to bind URI")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad Request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Str("client_id", req.ClientID).Msg("Client not found")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"status": 404,
|
||||||
|
"message": "Client not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"client": client.ClientID,
|
||||||
|
"name": client.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) Authorize(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
controller.authorizeError(c, errors.New("err_oidc_not_configured"), "OIDC not configured", "This instance is not configured for OIDC", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext, err := utils.GetContext(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req service.AuthorizeRequest
|
||||||
|
|
||||||
|
err = c.BindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to bind JSON", "The client provided an invalid authorization request", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
controller.authorizeError(c, err, "Client not found", "The client ID is invalid", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.oidc.ValidateAuthorizeParams(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to validate authorize params")
|
||||||
|
if err.Error() != "invalid_request_uri" {
|
||||||
|
controller.authorizeError(c, err, "Failed validate authorize params", "Invalid request parameters", req.RedirectURI, err.Error(), req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.authorizeError(c, err, "Redirect URI not trusted", "The provided redirect URI is not trusted", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
||||||
|
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.Username, client.ID))
|
||||||
|
code := rand.Text()
|
||||||
|
|
||||||
|
// Before storing the code, delete old session
|
||||||
|
err = controller.oidc.DeleteOldSession(c, sub)
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to delete old sessions", "Failed to delete old sessions", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.oidc.StoreCode(c, sub, code, req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to store code", "Failed to store code", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
||||||
|
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
||||||
|
err = controller.oidc.StoreUserinfo(c, sub, userContext, req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to insert user info into database")
|
||||||
|
controller.authorizeError(c, err, "Failed to store user info", "Failed to store user info", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(AuthorizeCallback{
|
||||||
|
Code: code,
|
||||||
|
State: req.State,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, err, "Failed to build query", "Failed to build query", req.RedirectURI, "server_error", req.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"redirect_uri": fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) Token(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
tlog.App.Warn().Msg("OIDC not configured")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "not_found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req TokenRequest
|
||||||
|
|
||||||
|
err := c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to bind token request")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.oidc.ValidateGrantType(req.GrantType)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Str("grant_type", req.GrantType).Msg("Unsupported grant type")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First we try form values
|
||||||
|
creds := ClientCredentials{
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
ClientSecret: req.ClientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it fails, we try basic auth
|
||||||
|
if creds.ClientID == "" || creds.ClientSecret == "" {
|
||||||
|
tlog.App.Debug().Msg("Tried form values and they are empty, trying basic auth")
|
||||||
|
|
||||||
|
clientId, clientSecret, ok := c.Request.BasicAuth()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Error().Msg("Missing authorization header")
|
||||||
|
c.Header("www-authenticate", "basic")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creds.ClientID = clientId
|
||||||
|
creds.ClientSecret = clientSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
// END - we don't support other authentication methods
|
||||||
|
|
||||||
|
client, ok := controller.oidc.GetClient(creds.ClientID)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Str("client_id", creds.ClientID).Msg("Client not found")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.ClientSecret != creds.ClientSecret {
|
||||||
|
tlog.App.Warn().Str("client_id", creds.ClientID).Msg("Invalid client secret")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResponse service.TokenResponse
|
||||||
|
|
||||||
|
switch req.GrantType {
|
||||||
|
case "authorization_code":
|
||||||
|
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, service.ErrCodeNotFound) {
|
||||||
|
tlog.App.Warn().Msg("Code not found")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, service.ErrCodeExpired) {
|
||||||
|
tlog.App.Warn().Msg("Code expired")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to get OIDC code entry")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.RedirectURI != req.RedirectURI {
|
||||||
|
tlog.App.Warn().Str("redirect_uri", req.RedirectURI).Msg("Redirect URI mismatch")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry.Sub, entry.Scope)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to generate access token")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse = tokenRes
|
||||||
|
case "refresh_token":
|
||||||
|
tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken, creds.ClientID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, service.ErrTokenExpired) {
|
||||||
|
tlog.App.Error().Err(err).Msg("Refresh token expired")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, service.ErrInvalidClient) {
|
||||||
|
tlog.App.Error().Err(err).Msg("Invalid client")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to refresh access token")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse = tokenRes
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, tokenResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
tlog.App.Warn().Msg("OIDC not configured")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "not_found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorization := c.GetHeader("Authorization")
|
||||||
|
|
||||||
|
tokenType, token, ok := strings.Cut(authorization, " ")
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(tokenType) != "bearer" {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == service.ErrTokenNotFound {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_grant",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Err(err).Msg("Failed to get token entry")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have the openid scope, return an error
|
||||||
|
if !slices.Contains(strings.Split(entry.Scope, ","), "openid") {
|
||||||
|
tlog.App.Warn().Msg("OIDC userinfo accessed without openid scope")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "invalid_scope",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := controller.oidc.GetUserinfo(c, entry.Sub)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Err(err).Msg("Failed to get user entry")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"error": "server_error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, controller.oidc.CompileUserinfo(user, entry.Scope))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) authorizeError(c *gin.Context, err error, reason string, reasonUser string, callback string, callbackError string, state string) {
|
||||||
|
tlog.App.Error().Err(err).Msg(reason)
|
||||||
|
|
||||||
|
if callback != "" {
|
||||||
|
errorQueries := CallbackError{
|
||||||
|
Error: callbackError,
|
||||||
|
}
|
||||||
|
|
||||||
|
if reasonUser != "" {
|
||||||
|
errorQueries.ErrorDescription = reasonUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != "" {
|
||||||
|
errorQueries.State = state
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(errorQueries)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"redirect_uri": fmt.Sprintf("%s?%s", callback, queries.Encode()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorQueries := ErrorScreen{
|
||||||
|
Error: reasonUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(errorQueries)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"redirect_uri": fmt.Sprintf("%s/error?%s", controller.oidc.GetIssuer(), queries.Encode()),
|
||||||
|
})
|
||||||
|
}
|
||||||
281
internal/controller/oidc_controller_test.go
Normal file
281
internal/controller/oidc_controller_test.go
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
package controller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var oidcServiceConfig = service.OIDCServiceConfig{
|
||||||
|
Clients: map[string]config.OIDCClientConfig{
|
||||||
|
"client1": {
|
||||||
|
ClientID: "some-client-id",
|
||||||
|
ClientSecret: "some-client-secret",
|
||||||
|
ClientSecretFile: "",
|
||||||
|
TrustedRedirectURIs: []string{
|
||||||
|
"https://example.com/oauth/callback",
|
||||||
|
},
|
||||||
|
Name: "Client 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrivateKeyPath: "/tmp/tinyauth_oidc_key",
|
||||||
|
PublicKeyPath: "/tmp/tinyauth_oidc_key.pub",
|
||||||
|
Issuer: "https://example.com",
|
||||||
|
SessionExpiry: 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
var oidcCtrlTestContext = config.UserContext{
|
||||||
|
Username: "test",
|
||||||
|
Name: "Test",
|
||||||
|
Email: "test@example.com",
|
||||||
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: false,
|
||||||
|
OAuth: false,
|
||||||
|
Provider: "ldap", // ldap in order to test the groups
|
||||||
|
TotpPending: false,
|
||||||
|
OAuthGroups: "",
|
||||||
|
TotpEnabled: false,
|
||||||
|
OAuthName: "",
|
||||||
|
OAuthSub: "",
|
||||||
|
LdapGroups: "test1,test2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test is not amazing, but it will confirm the OIDC server works
|
||||||
|
func TestOIDCController(t *testing.T) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
|
// Create an app instance
|
||||||
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
|
// Get db
|
||||||
|
db, err := app.SetupDatabase("/tmp/tinyauth.db")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Create queries
|
||||||
|
queries := repository.New(db)
|
||||||
|
|
||||||
|
// Create a new OIDC Servicee
|
||||||
|
oidcService := service.NewOIDCService(oidcServiceConfig, queries)
|
||||||
|
err = oidcService.Init()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Create test router
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("context", &oidcCtrlTestContext)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
group := router.Group("/api")
|
||||||
|
|
||||||
|
// Register oidc controller
|
||||||
|
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, oidcService, group)
|
||||||
|
oidcController.SetupRoutes()
|
||||||
|
|
||||||
|
// Get redirect URL test
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
marshalled, err := json.Marshal(service.AuthorizeRequest{
|
||||||
|
Scope: "openid profile email groups",
|
||||||
|
ResponseType: "code",
|
||||||
|
ClientID: "some-client-id",
|
||||||
|
RedirectURI: "https://example.com/oauth/callback",
|
||||||
|
State: "some-state",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(marshalled)))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson := map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
redirect_uri, ok := resJson["redirect_uri"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
u, err := url.Parse(redirect_uri)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
m, err := url.ParseQuery(u.RawQuery)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, m["state"][0], "some-state")
|
||||||
|
|
||||||
|
code := m["code"][0]
|
||||||
|
|
||||||
|
// Exchange code for token
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
params, err := query.Values(controller.TokenRequest{
|
||||||
|
GrantType: "authorization_code",
|
||||||
|
Code: code,
|
||||||
|
RedirectURI: "https://example.com/oauth/callback",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson = map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
accessToken, ok := resJson["access_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
_, ok = resJson["id_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
refreshToken, ok := resJson["refresh_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
expires_in, ok := resJson["expires_in"].(float64)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, expires_in, float64(oidcServiceConfig.SessionExpiry))
|
||||||
|
|
||||||
|
// Ensure code is expired
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
params, err = query.Values(controller.TokenRequest{
|
||||||
|
GrantType: "authorization_code",
|
||||||
|
Code: code,
|
||||||
|
RedirectURI: "https://example.com/oauth/callback",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
// Test userinfo
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson = map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
_, ok = resJson["sub"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
name, ok := resJson["name"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, name, oidcCtrlTestContext.Name)
|
||||||
|
|
||||||
|
email, ok := resJson["email"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, email, oidcCtrlTestContext.Email)
|
||||||
|
|
||||||
|
preferred_username, ok := resJson["preferred_username"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Equal(t, preferred_username, oidcCtrlTestContext.Username)
|
||||||
|
|
||||||
|
// Not sure why this is failing, will look into it later
|
||||||
|
igroups, ok := resJson["groups"].([]any)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
|
||||||
|
groups := make([]string, len(igroups))
|
||||||
|
for i, group := range igroups {
|
||||||
|
groups[i], ok = group.(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.DeepEqual(t, strings.Split(oidcCtrlTestContext.LdapGroups, ","), groups)
|
||||||
|
|
||||||
|
// Test refresh token
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
|
||||||
|
params, err = query.Values(controller.TokenRequest{
|
||||||
|
GrantType: "refresh_token",
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
resJson = map[string]any{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
newToken, ok := resJson["access_token"].(string)
|
||||||
|
assert.Assert(t, ok)
|
||||||
|
assert.Assert(t, newToken != accessToken)
|
||||||
|
|
||||||
|
// Ensure old token is invalid
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
|
|
||||||
|
// Test new token
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", newToken))
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
}
|
||||||
@@ -173,7 +173,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||||
|
|
||||||
if userContext.Provider == "basic" && userContext.TotpEnabled {
|
if userContext.IsBasicAuth && userContext.TotpEnabled {
|
||||||
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
||||||
userContext.IsLoggedIn = false
|
userContext.IsLoggedIn = false
|
||||||
}
|
}
|
||||||
@@ -212,11 +212,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.OAuth {
|
if userContext.OAuth || userContext.Provider == "ldap" {
|
||||||
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
var groupOK bool
|
||||||
|
|
||||||
|
if userContext.OAuth {
|
||||||
|
groupOK = controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
||||||
|
} else {
|
||||||
|
groupOK = controller.auth.IsInLdapGroup(c, userContext, acls.LDAP.Groups)
|
||||||
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User groups do not match resource requirements")
|
||||||
|
|
||||||
if req.Proxy == "nginx" || !isBrowser {
|
if req.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, gin.H{
|
||||||
@@ -251,7 +257,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
||||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
|
||||||
|
if userContext.Provider == "ldap" {
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.LdapGroups))
|
||||||
|
} else if userContext.Provider != "local" {
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||||
|
}
|
||||||
|
|
||||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
||||||
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
|
|||||||
@@ -143,11 +143,11 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
// Test logged in user
|
// Test logged in user
|
||||||
c := gin.CreateTestContextOnly(recorder, router)
|
c := gin.CreateTestContextOnly(recorder, router)
|
||||||
|
|
||||||
err := authService.CreateSessionCookie(c, &config.SessionCookie{
|
err := authService.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
})
|
})
|
||||||
@@ -164,7 +164,7 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: false,
|
TotpEnabled: false,
|
||||||
@@ -192,8 +192,9 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: true,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "basic",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
@@ -112,11 +111,11 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
if user.TotpSecret != "" {
|
if user.TotpSecret != "" {
|
||||||
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||||
|
|
||||||
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{
|
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -138,11 +137,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(req.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(req.Username, controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSearch.Type == "ldap" {
|
||||||
|
sessionCookie.Provider = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
@@ -248,11 +251,11 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
controller.auth.RecordLoginAttempt(context.Username, true)
|
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func TestTotpHandler(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
@@ -267,7 +267,7 @@ func TestTotpHandler(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
@@ -290,7 +290,7 @@ func TestTotpHandler(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
OAuth: false,
|
OAuth: false,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: false,
|
TotpPending: false,
|
||||||
OAuthGroups: "",
|
OAuthGroups: "",
|
||||||
TotpEnabled: false,
|
TotpEnabled: false,
|
||||||
|
|||||||
85
internal/controller/well_known_controller.go
Normal file
85
internal/controller/well_known_controller.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenIDConnectConfiguration struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||||
|
JwksUri string `json:"jwks_uri"`
|
||||||
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
|
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||||
|
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||||
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
|
ServiceDocumentation string `json:"service_documentation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WellKnownControllerConfig struct{}
|
||||||
|
|
||||||
|
type WellKnownController struct {
|
||||||
|
config WellKnownControllerConfig
|
||||||
|
engine *gin.Engine
|
||||||
|
oidc *service.OIDCService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWellKnownController(config WellKnownControllerConfig, oidc *service.OIDCService, engine *gin.Engine) *WellKnownController {
|
||||||
|
return &WellKnownController{
|
||||||
|
config: config,
|
||||||
|
oidc: oidc,
|
||||||
|
engine: engine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) SetupRoutes() {
|
||||||
|
controller.engine.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
||||||
|
controller.engine.GET("/.well-known/jwks.json", controller.JWKS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
|
||||||
|
issuer := controller.oidc.GetIssuer()
|
||||||
|
c.JSON(200, OpenIDConnectConfiguration{
|
||||||
|
Issuer: issuer,
|
||||||
|
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", issuer),
|
||||||
|
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", issuer),
|
||||||
|
UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", issuer),
|
||||||
|
JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", issuer),
|
||||||
|
ScopesSupported: service.SupportedScopes,
|
||||||
|
ResponseTypesSupported: service.SupportedResponseTypes,
|
||||||
|
GrantTypesSupported: service.SupportedGrantTypes,
|
||||||
|
SubjectTypesSupported: []string{"pairwise"},
|
||||||
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||||
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
||||||
|
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "groups"},
|
||||||
|
ServiceDocumentation: "https://tinyauth.app/docs/reference/openid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) JWKS(c *gin.Context) {
|
||||||
|
jwks, err := controller.oidc.GetJWK()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": "500",
|
||||||
|
"message": "failed to get JWK",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("content-type", "application/json")
|
||||||
|
|
||||||
|
c.Writer.WriteString(`{"keys":[`)
|
||||||
|
c.Writer.Write(jwks)
|
||||||
|
c.Writer.WriteString(`]}`)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var OIDCIgnorePaths = []string{"/api/oidc/token", "/api/oidc/userinfo"}
|
||||||
|
|
||||||
type ContextMiddlewareConfig struct {
|
type ContextMiddlewareConfig struct {
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,13 @@ func (m *ContextMiddleware) Init() error {
|
|||||||
|
|
||||||
func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
// There is no point in trying to get credentials if it's an OIDC endpoint
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if slices.Contains(OIDCIgnorePaths, strings.TrimSuffix(path, "/")) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := m.auth.GetSessionCookie(c)
|
cookie, err := m.auth.GetSessionCookie(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +58,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
Name: cookie.Name,
|
Name: cookie.Name,
|
||||||
Email: cookie.Email,
|
Email: cookie.Email,
|
||||||
Provider: "username",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
TotpEnabled: true,
|
TotpEnabled: true,
|
||||||
})
|
})
|
||||||
@@ -58,22 +67,44 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch cookie.Provider {
|
switch cookie.Provider {
|
||||||
case "username":
|
case "local", "ldap":
|
||||||
userSearch := m.auth.SearchUser(cookie.Username)
|
userSearch := m.auth.SearchUser(cookie.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
if userSearch.Type == "unknown" {
|
||||||
tlog.App.Debug().Msg("User from session cookie not found")
|
tlog.App.Debug().Msg("User from session cookie not found")
|
||||||
m.auth.DeleteSessionCookie(c)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userSearch.Type != cookie.Provider {
|
||||||
|
tlog.App.Warn().Msg("User type from session cookie does not match user search type")
|
||||||
|
m.auth.DeleteSessionCookie(c)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ldapGroups []string
|
||||||
|
|
||||||
|
if cookie.Provider == "ldap" {
|
||||||
|
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapGroups = ldapUser.Groups
|
||||||
|
}
|
||||||
|
|
||||||
m.auth.RefreshSessionCookie(c)
|
m.auth.RefreshSessionCookie(c)
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
Name: cookie.Name,
|
Name: cookie.Name,
|
||||||
Email: cookie.Email,
|
Email: cookie.Email,
|
||||||
Provider: "username",
|
Provider: cookie.Provider,
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: strings.Join(ldapGroups, ","),
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -154,21 +185,33 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
||||||
Provider: "basic",
|
Provider: "local",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
TotpEnabled: user.TotpSecret != "",
|
TotpEnabled: user.TotpSecret != "",
|
||||||
|
IsBasicAuth: true,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
case "ldap":
|
case "ldap":
|
||||||
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
||||||
|
|
||||||
|
ldapUser, err := m.auth.GetLdapUser(basic.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: basic.Username,
|
Username: basic.Username,
|
||||||
Name: utils.Capitalize(basic.Username),
|
Name: utils.Capitalize(basic.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||||
Provider: "basic",
|
Provider: "ldap",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||||
|
IsBasicAuth: true,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
"github.com/steveiliop56/tinyauth/internal/assets"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -39,11 +40,10 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||||
|
|
||||||
|
tlog.App.Debug().Str("path", path).Msg("path")
|
||||||
|
|
||||||
switch strings.SplitN(path, "/", 2)[0] {
|
switch strings.SplitN(path, "/", 2)[0] {
|
||||||
case "api":
|
case "api", "resources", ".well-known":
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
case "resources":
|
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -4,6 +4,34 @@
|
|||||||
|
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
|
type OidcCode struct {
|
||||||
|
Sub string
|
||||||
|
CodeHash string
|
||||||
|
Scope string
|
||||||
|
RedirectURI string
|
||||||
|
ClientID string
|
||||||
|
ExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcToken struct {
|
||||||
|
Sub string
|
||||||
|
AccessTokenHash string
|
||||||
|
RefreshTokenHash string
|
||||||
|
Scope string
|
||||||
|
ClientID string
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcUserinfo struct {
|
||||||
|
Sub string
|
||||||
|
Name string
|
||||||
|
PreferredUsername string
|
||||||
|
Email string
|
||||||
|
Groups string
|
||||||
|
UpdatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
UUID string
|
UUID string
|
||||||
Username string
|
Username string
|
||||||
|
|||||||
470
internal/repository/oidc_queries.sql.go
Normal file
470
internal/repository/oidc_queries.sql.go
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: oidc_queries.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createOidcCode = `-- name: CreateOidcCode :one
|
||||||
|
INSERT INTO "oidc_codes" (
|
||||||
|
"sub",
|
||||||
|
"code_hash",
|
||||||
|
"scope",
|
||||||
|
"redirect_uri",
|
||||||
|
"client_id",
|
||||||
|
"expires_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOidcCodeParams struct {
|
||||||
|
Sub string
|
||||||
|
CodeHash string
|
||||||
|
Scope string
|
||||||
|
RedirectURI string
|
||||||
|
ClientID string
|
||||||
|
ExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createOidcCode,
|
||||||
|
arg.Sub,
|
||||||
|
arg.CodeHash,
|
||||||
|
arg.Scope,
|
||||||
|
arg.RedirectURI,
|
||||||
|
arg.ClientID,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOidcToken = `-- name: CreateOidcToken :one
|
||||||
|
INSERT INTO "oidc_tokens" (
|
||||||
|
"sub",
|
||||||
|
"access_token_hash",
|
||||||
|
"refresh_token_hash",
|
||||||
|
"scope",
|
||||||
|
"client_id",
|
||||||
|
"token_expires_at",
|
||||||
|
"refresh_token_expires_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOidcTokenParams struct {
|
||||||
|
Sub string
|
||||||
|
AccessTokenHash string
|
||||||
|
RefreshTokenHash string
|
||||||
|
Scope string
|
||||||
|
ClientID string
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createOidcToken,
|
||||||
|
arg.Sub,
|
||||||
|
arg.AccessTokenHash,
|
||||||
|
arg.RefreshTokenHash,
|
||||||
|
arg.Scope,
|
||||||
|
arg.ClientID,
|
||||||
|
arg.TokenExpiresAt,
|
||||||
|
arg.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOidcUserInfo = `-- name: CreateOidcUserInfo :one
|
||||||
|
INSERT INTO "oidc_userinfo" (
|
||||||
|
"sub",
|
||||||
|
"name",
|
||||||
|
"preferred_username",
|
||||||
|
"email",
|
||||||
|
"groups",
|
||||||
|
"updated_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING sub, name, preferred_username, email, "groups", updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOidcUserInfoParams struct {
|
||||||
|
Sub string
|
||||||
|
Name string
|
||||||
|
PreferredUsername string
|
||||||
|
Email string
|
||||||
|
Groups string
|
||||||
|
UpdatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createOidcUserInfo,
|
||||||
|
arg.Sub,
|
||||||
|
arg.Name,
|
||||||
|
arg.PreferredUsername,
|
||||||
|
arg.Email,
|
||||||
|
arg.Groups,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
var i OidcUserinfo
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.Name,
|
||||||
|
&i.PreferredUsername,
|
||||||
|
&i.Email,
|
||||||
|
&i.Groups,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "expires_at" < ?
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, deleteExpiredOidcCodes, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []OidcCode
|
||||||
|
for rows.Next() {
|
||||||
|
var i OidcCode
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredOidcTokens = `-- name: DeleteExpiredOidcTokens :many
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "token_expires_at" < ? AND "refresh_token_expires_at" < ?
|
||||||
|
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteExpiredOidcTokensParams struct {
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpiredOidcTokensParams) ([]OidcToken, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, deleteExpiredOidcTokens, arg.TokenExpiresAt, arg.RefreshTokenExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []OidcToken
|
||||||
|
for rows.Next() {
|
||||||
|
var i OidcToken
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcCode = `-- name: DeleteOidcCode :exec
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcCode(ctx context.Context, codeHash string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcCode, codeHash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcCodeBySub = `-- name: DeleteOidcCodeBySub :exec
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcCodeBySub(ctx context.Context, sub string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcCodeBySub, sub)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcToken = `-- name: DeleteOidcToken :exec
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "access_token_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcToken(ctx context.Context, accessTokenHash string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcToken, accessTokenHash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcTokenBySub = `-- name: DeleteOidcTokenBySub :exec
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcTokenBySub(ctx context.Context, sub string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcTokenBySub, sub)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOidcUserInfo = `-- name: DeleteOidcUserInfo :exec
|
||||||
|
DELETE FROM "oidc_userinfo"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteOidcUserInfo, sub)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCode = `-- name: GetOidcCode :one
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCode, codeHash)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCodeBySub, sub)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
|
||||||
|
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCodeBySubUnsafe, sub)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
|
||||||
|
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcCode, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcCodeUnsafe, codeHash)
|
||||||
|
var i OidcCode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.CodeHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.RedirectURI,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcToken = `-- name: GetOidcToken :one
|
||||||
|
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
|
||||||
|
WHERE "access_token_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcToken(ctx context.Context, accessTokenHash string) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcToken, accessTokenHash)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcTokenByRefreshToken = `-- name: GetOidcTokenByRefreshToken :one
|
||||||
|
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
|
||||||
|
WHERE "refresh_token_hash" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcTokenByRefreshToken, refreshTokenHash)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcTokenBySub = `-- name: GetOidcTokenBySub :one
|
||||||
|
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcTokenBySub, sub)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
|
||||||
|
SELECT sub, name, preferred_username, email, "groups", updated_at FROM "oidc_userinfo"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getOidcUserInfo, sub)
|
||||||
|
var i OidcUserinfo
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.Name,
|
||||||
|
&i.PreferredUsername,
|
||||||
|
&i.Email,
|
||||||
|
&i.Groups,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOidcTokenByRefreshToken = `-- name: UpdateOidcTokenByRefreshToken :one
|
||||||
|
UPDATE "oidc_tokens" SET
|
||||||
|
"access_token_hash" = ?,
|
||||||
|
"refresh_token_hash" = ?,
|
||||||
|
"token_expires_at" = ?,
|
||||||
|
"refresh_token_expires_at" = ?
|
||||||
|
WHERE "refresh_token_hash" = ?
|
||||||
|
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateOidcTokenByRefreshTokenParams struct {
|
||||||
|
AccessTokenHash string
|
||||||
|
RefreshTokenHash string
|
||||||
|
TokenExpiresAt int64
|
||||||
|
RefreshTokenExpiresAt int64
|
||||||
|
RefreshTokenHash_2 string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateOidcTokenByRefreshTokenParams) (OidcToken, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateOidcTokenByRefreshToken,
|
||||||
|
arg.AccessTokenHash,
|
||||||
|
arg.RefreshTokenHash,
|
||||||
|
arg.TokenExpiresAt,
|
||||||
|
arg.RefreshTokenExpiresAt,
|
||||||
|
arg.RefreshTokenHash_2,
|
||||||
|
)
|
||||||
|
var i OidcToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Sub,
|
||||||
|
&i.AccessTokenHash,
|
||||||
|
&i.RefreshTokenHash,
|
||||||
|
&i.Scope,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.TokenExpiresAt,
|
||||||
|
&i.RefreshTokenExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.30.0
|
||||||
// source: queries.sql
|
// source: session_queries.sql
|
||||||
|
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const createSession = `-- name: CreateSession :one
|
const createSession = `-- name: CreateSession :one
|
||||||
INSERT INTO sessions (
|
INSERT INTO "sessions" (
|
||||||
"uuid",
|
"uuid",
|
||||||
"username",
|
"username",
|
||||||
"email",
|
"email",
|
||||||
@@ -19,6 +19,11 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type LdapGroupsCache struct {
|
||||||
|
Groups []string
|
||||||
|
Expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type LoginAttempt struct {
|
type LoginAttempt struct {
|
||||||
FailedAttempts int
|
FailedAttempts int
|
||||||
LastAttempt time.Time
|
LastAttempt time.Time
|
||||||
@@ -36,24 +41,28 @@ type AuthServiceConfig struct {
|
|||||||
LoginMaxRetries int
|
LoginMaxRetries int
|
||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
IP config.IPConfig
|
IP config.IPConfig
|
||||||
|
LDAPGroupsCacheTTL int
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
config AuthServiceConfig
|
config AuthServiceConfig
|
||||||
docker *DockerService
|
docker *DockerService
|
||||||
loginAttempts map[string]*LoginAttempt
|
loginAttempts map[string]*LoginAttempt
|
||||||
loginMutex sync.RWMutex
|
ldapGroupsCache map[string]*LdapGroupsCache
|
||||||
ldap *LdapService
|
loginMutex sync.RWMutex
|
||||||
queries *repository.Queries
|
ldapGroupsMutex sync.RWMutex
|
||||||
|
ldap *LdapService
|
||||||
|
queries *repository.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries) *AuthService {
|
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
config: config,
|
config: config,
|
||||||
docker: docker,
|
docker: docker,
|
||||||
loginAttempts: make(map[string]*LoginAttempt),
|
loginAttempts: make(map[string]*LoginAttempt),
|
||||||
ldap: ldap,
|
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||||
queries: queries,
|
ldap: ldap,
|
||||||
|
queries: queries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,13 +78,13 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.ldap != nil {
|
if auth.ldap.IsConfigured() {
|
||||||
userDN, err := auth.ldap.Search(username)
|
userDN, err := auth.ldap.GetUserDN(username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
||||||
return config.UserSearch{
|
return config.UserSearch{
|
||||||
Type: "error",
|
Type: "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +105,7 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
|
|||||||
user := auth.GetLocalUser(search.Username)
|
user := auth.GetLocalUser(search.Username)
|
||||||
return auth.CheckPassword(user, password)
|
return auth.CheckPassword(user, password)
|
||||||
case "ldap":
|
case "ldap":
|
||||||
if auth.ldap != nil {
|
if auth.ldap.IsConfigured() {
|
||||||
err := auth.ldap.Bind(search.Username, password)
|
err := auth.ldap.Bind(search.Username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
||||||
@@ -131,6 +140,41 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
|
|||||||
return config.User{}
|
return config.User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
||||||
|
if !auth.ldap.IsConfigured() {
|
||||||
|
return config.LdapUser{}, errors.New("LDAP service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.ldapGroupsMutex.RLock()
|
||||||
|
entry, exists := auth.ldapGroupsCache[userDN]
|
||||||
|
auth.ldapGroupsMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists && time.Now().Before(entry.Expires) {
|
||||||
|
return config.LdapUser{
|
||||||
|
DN: userDN,
|
||||||
|
Groups: entry.Groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := auth.ldap.GetUserGroups(userDN)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return config.LdapUser{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.ldapGroupsMutex.Lock()
|
||||||
|
auth.ldapGroupsCache[userDN] = &LdapGroupsCache{
|
||||||
|
Groups: groups,
|
||||||
|
Expires: time.Now().Add(time.Duration(auth.config.LDAPGroupsCacheTTL) * time.Second),
|
||||||
|
}
|
||||||
|
auth.ldapGroupsMutex.Unlock()
|
||||||
|
|
||||||
|
return config.LdapUser{
|
||||||
|
DN: userDN,
|
||||||
|
Groups: groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||||
}
|
}
|
||||||
@@ -190,7 +234,7 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
|||||||
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
|
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -300,20 +344,20 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) {
|
func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
|
||||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.SessionCookie{}, err
|
return repository.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := auth.queries.GetSession(c, cookie)
|
session, err := auth.queries.GetSession(c, cookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return config.SessionCookie{}, fmt.Errorf("session not found")
|
return repository.Session{}, fmt.Errorf("session not found")
|
||||||
}
|
}
|
||||||
return config.SessionCookie{}, err
|
return repository.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
@@ -324,7 +368,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
||||||
}
|
}
|
||||||
return config.SessionCookie{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,10 +377,10 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
|
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
|
||||||
}
|
}
|
||||||
return config.SessionCookie{}, fmt.Errorf("session expired")
|
return repository.Session{}, fmt.Errorf("session expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.SessionCookie{
|
return repository.Session{
|
||||||
UUID: session.UUID,
|
UUID: session.UUID,
|
||||||
Username: session.Username,
|
Username: session.Username,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
@@ -349,8 +393,12 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) UserAuthConfigured() bool {
|
func (auth *AuthService) LocalAuthConfigured() bool {
|
||||||
return len(auth.config.Users) > 0 || auth.ldap != nil
|
return len(auth.config.Users) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) LdapAuthConfigured() bool {
|
||||||
|
return auth.ldap.IsConfigured()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||||
@@ -393,6 +441,22 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
|
||||||
|
if requiredGroups == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for userGroup := range strings.SplitSeq(context.LdapGroups, ",") {
|
||||||
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Debug().Msg("No groups matched")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) {
|
func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) {
|
||||||
// Check for block list
|
// Check for block list
|
||||||
if path.Block != "" {
|
if path.Block != "" {
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ type LdapServiceConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LdapService struct {
|
type LdapService struct {
|
||||||
config LdapServiceConfig
|
config LdapServiceConfig
|
||||||
conn *ldapgo.Conn
|
conn *ldapgo.Conn
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
cert *tls.Certificate
|
cert *tls.Certificate
|
||||||
|
isConfigured bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLdapService(config LdapServiceConfig) *LdapService {
|
func NewLdapService(config LdapServiceConfig) *LdapService {
|
||||||
@@ -36,7 +37,33 @@ func NewLdapService(config LdapServiceConfig) *LdapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) IsConfigured() bool {
|
||||||
|
return ldap.isConfigured
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) Unconfigure() error {
|
||||||
|
if !ldap.isConfigured {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ldap.conn != nil {
|
||||||
|
if err := ldap.conn.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close LDAP connection: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ldap.isConfigured = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Init() error {
|
func (ldap *LdapService) Init() error {
|
||||||
|
if ldap.config.Address == "" {
|
||||||
|
ldap.isConfigured = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ldap.isConfigured = true
|
||||||
|
|
||||||
// Check whether authentication with client certificate is possible
|
// Check whether authentication with client certificate is possible
|
||||||
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
|
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
|
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
|
||||||
@@ -116,7 +143,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
|
|||||||
return ldap.conn, nil
|
return ldap.conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Search(username string) (string, error) {
|
func (ldap *LdapService) GetUserDN(username string) (string, error) {
|
||||||
// Escape the username to prevent LDAP injection
|
// Escape the username to prevent LDAP injection
|
||||||
escapedUsername := ldapgo.EscapeFilter(username)
|
escapedUsername := ldapgo.EscapeFilter(username)
|
||||||
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
|
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
|
||||||
@@ -145,6 +172,48 @@ func (ldap *LdapService) Search(username string) (string, error) {
|
|||||||
return userDN, nil
|
return userDN, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
||||||
|
escapedUserDN := ldapgo.EscapeFilter(userDN)
|
||||||
|
|
||||||
|
searchRequest := ldapgo.NewSearchRequest(
|
||||||
|
ldap.config.BaseDN,
|
||||||
|
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||||
|
fmt.Sprintf("(&(objectclass=groupOfUniqueNames)(uniquemember=%s))", escapedUserDN),
|
||||||
|
[]string{"dn"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
ldap.mutex.Lock()
|
||||||
|
defer ldap.mutex.Unlock()
|
||||||
|
|
||||||
|
searchResult, err := ldap.conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupDNs := []string{}
|
||||||
|
|
||||||
|
for _, entry := range searchResult.Entries {
|
||||||
|
groupDNs = append(groupDNs, entry.DN)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := []string{}
|
||||||
|
|
||||||
|
// I guess it should work for most ldap providers
|
||||||
|
for _, dn := range groupDNs {
|
||||||
|
rdnParts, err := ldapgo.ParseDN(dn)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
if len(rdnParts.RDNs) == 0 || len(rdnParts.RDNs[0].Attributes) == 0 {
|
||||||
|
return []string{}, fmt.Errorf("invalid DN format: %s", dn)
|
||||||
|
}
|
||||||
|
groups = append(groups, rdnParts.RDNs[0].Attributes[0].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) BindService(rebind bool) error {
|
func (ldap *LdapService) BindService(rebind bool) error {
|
||||||
// Locks must not be used for initial binding attempt
|
// Locks must not be used for initial binding attempt
|
||||||
if rebind {
|
if rebind {
|
||||||
|
|||||||
653
internal/service/oidc_service.go
Normal file
653
internal/service/oidc_service.go
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
SupportedScopes = []string{"openid", "profile", "email", "groups"}
|
||||||
|
SupportedResponseTypes = []string{"code"}
|
||||||
|
SupportedGrantTypes = []string{"authorization_code", "refresh_token"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCodeExpired = errors.New("code_expired")
|
||||||
|
ErrCodeNotFound = errors.New("code_not_found")
|
||||||
|
ErrTokenNotFound = errors.New("token_not_found")
|
||||||
|
ErrTokenExpired = errors.New("token_expired")
|
||||||
|
ErrInvalidClient = errors.New("invalid_client")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClaimSet struct {
|
||||||
|
Iss string `json:"iss"`
|
||||||
|
Aud string `json:"aud"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Iat int64 `json:"iat"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserinfoResponse struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizeRequest struct {
|
||||||
|
Scope string `json:"scope" binding:"required"`
|
||||||
|
ResponseType string `json:"response_type" binding:"required"`
|
||||||
|
ClientID string `json:"client_id" binding:"required"`
|
||||||
|
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||||
|
State string `json:"state" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCServiceConfig struct {
|
||||||
|
Clients map[string]config.OIDCClientConfig
|
||||||
|
PrivateKeyPath string
|
||||||
|
PublicKeyPath string
|
||||||
|
Issuer string
|
||||||
|
SessionExpiry int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCService struct {
|
||||||
|
config OIDCServiceConfig
|
||||||
|
queries *repository.Queries
|
||||||
|
clients map[string]config.OIDCClientConfig
|
||||||
|
privateKey *rsa.PrivateKey
|
||||||
|
publicKey crypto.PublicKey
|
||||||
|
issuer string
|
||||||
|
isConfigured bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDCService {
|
||||||
|
return &OIDCService{
|
||||||
|
config: config,
|
||||||
|
queries: queries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) IsConfigured() bool {
|
||||||
|
return service.isConfigured
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) Init() error {
|
||||||
|
// If not configured, skip init
|
||||||
|
if len(service.config.Clients) == 0 {
|
||||||
|
service.isConfigured = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
service.isConfigured = true
|
||||||
|
|
||||||
|
// Ensure issuer is https
|
||||||
|
uissuer, err := url.Parse(service.config.Issuer)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if uissuer.Scheme != "https" {
|
||||||
|
return errors.New("issuer must be https")
|
||||||
|
}
|
||||||
|
|
||||||
|
service.issuer = fmt.Sprintf("%s://%s", uissuer.Scheme, uissuer.Host)
|
||||||
|
|
||||||
|
// Create/load private and public keys
|
||||||
|
if strings.TrimSpace(service.config.PrivateKeyPath) == "" ||
|
||||||
|
strings.TrimSpace(service.config.PublicKeyPath) == "" {
|
||||||
|
return errors.New("private key path and public key path are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKey *rsa.PrivateKey
|
||||||
|
|
||||||
|
fprivateKey, err := os.ReadFile(service.config.PrivateKeyPath)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
der := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||||
|
if der == nil {
|
||||||
|
return errors.New("failed to marshal private key")
|
||||||
|
}
|
||||||
|
encoded := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: der,
|
||||||
|
})
|
||||||
|
err = os.WriteFile(service.config.PrivateKeyPath, encoded, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.privateKey = privateKey
|
||||||
|
} else {
|
||||||
|
block, _ := pem.Decode(fprivateKey)
|
||||||
|
if block == nil {
|
||||||
|
return errors.New("failed to decode private key")
|
||||||
|
}
|
||||||
|
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.privateKey = privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fpublicKey, err := os.ReadFile(service.config.PublicKeyPath)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
publicKey := service.privateKey.Public()
|
||||||
|
der := x509.MarshalPKCS1PublicKey(publicKey.(*rsa.PublicKey))
|
||||||
|
if der == nil {
|
||||||
|
return errors.New("failed to marshal public key")
|
||||||
|
}
|
||||||
|
encoded := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PUBLIC KEY",
|
||||||
|
Bytes: der,
|
||||||
|
})
|
||||||
|
err = os.WriteFile(service.config.PublicKeyPath, encoded, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.publicKey = publicKey
|
||||||
|
} else {
|
||||||
|
block, _ := pem.Decode(fpublicKey)
|
||||||
|
if block == nil {
|
||||||
|
return errors.New("failed to decode public key")
|
||||||
|
}
|
||||||
|
publicKey, err := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.publicKey = publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will reorganize the client into a map with the client ID as the key
|
||||||
|
service.clients = make(map[string]config.OIDCClientConfig)
|
||||||
|
|
||||||
|
for id, client := range service.config.Clients {
|
||||||
|
client.ID = id
|
||||||
|
service.clients[client.ClientID] = client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the client secrets from files if they exist
|
||||||
|
for id, client := range service.clients {
|
||||||
|
secret := utils.GetSecret(client.ClientSecret, client.ClientSecretFile)
|
||||||
|
if secret != "" {
|
||||||
|
client.ClientSecret = secret
|
||||||
|
}
|
||||||
|
client.ClientSecretFile = ""
|
||||||
|
service.clients[id] = client
|
||||||
|
tlog.App.Info().Str("id", client.ID).Msg("Registered OIDC client")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetIssuer() string {
|
||||||
|
return service.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetClient(id string) (config.OIDCClientConfig, bool) {
|
||||||
|
client, ok := service.clients[id]
|
||||||
|
return client, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) ValidateAuthorizeParams(req AuthorizeRequest) error {
|
||||||
|
// Validate client ID
|
||||||
|
client, ok := service.GetClient(req.ClientID)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("access_denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes
|
||||||
|
scopes := strings.Split(req.Scope, " ")
|
||||||
|
|
||||||
|
if len(scopes) == 0 || strings.TrimSpace(req.Scope) == "" {
|
||||||
|
return errors.New("invalid_scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if strings.TrimSpace(scope) == "" {
|
||||||
|
return errors.New("invalid_scope")
|
||||||
|
}
|
||||||
|
if !slices.Contains(SupportedScopes, scope) {
|
||||||
|
tlog.App.Warn().Str("scope", scope).Msg("Unsupported OIDC scope, will be ignored")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response type
|
||||||
|
if !slices.Contains(SupportedResponseTypes, req.ResponseType) {
|
||||||
|
return errors.New("unsupported_response_type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect URI
|
||||||
|
if !slices.Contains(client.TrustedRedirectURIs, req.RedirectURI) {
|
||||||
|
return errors.New("invalid_request_uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) filterScopes(scopes []string) []string {
|
||||||
|
return utils.Filter(scopes, func(scope string) bool {
|
||||||
|
return slices.Contains(SupportedScopes, scope)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, req AuthorizeRequest) error {
|
||||||
|
// Fixed 10 minutes
|
||||||
|
expiresAt := time.Now().Add(time.Minute * time.Duration(10)).Unix()
|
||||||
|
|
||||||
|
// Insert the code into the database
|
||||||
|
_, err := service.queries.CreateOidcCode(c, repository.CreateOidcCodeParams{
|
||||||
|
Sub: sub,
|
||||||
|
CodeHash: service.Hash(code),
|
||||||
|
// Here it's safe to split and trust the output since, we validated the scopes before
|
||||||
|
Scope: strings.Join(service.filterScopes(strings.Split(req.Scope, " ")), ","),
|
||||||
|
RedirectURI: req.RedirectURI,
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext config.UserContext, req AuthorizeRequest) error {
|
||||||
|
userInfoParams := repository.CreateOidcUserInfoParams{
|
||||||
|
Sub: sub,
|
||||||
|
Name: userContext.Name,
|
||||||
|
Email: userContext.Email,
|
||||||
|
PreferredUsername: userContext.Username,
|
||||||
|
UpdatedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server
|
||||||
|
if userContext.Provider == "ldap" {
|
||||||
|
userInfoParams.Groups = userContext.LdapGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if userContext.OAuth && len(userContext.OAuthGroups) > 0 {
|
||||||
|
userInfoParams.Groups = userContext.OAuthGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := service.queries.CreateOidcUserInfo(c, userInfoParams)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) ValidateGrantType(grantType string) error {
|
||||||
|
if !slices.Contains(SupportedGrantTypes, grantType) {
|
||||||
|
return errors.New("unsupported_grant_type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repository.OidcCode, error) {
|
||||||
|
oidcCode, err := service.queries.GetOidcCode(c, codeHash)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return repository.OidcCode{}, ErrCodeNotFound
|
||||||
|
}
|
||||||
|
return repository.OidcCode{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Unix() > oidcCode.ExpiresAt {
|
||||||
|
err = service.queries.DeleteOidcCode(c, codeHash)
|
||||||
|
if err != nil {
|
||||||
|
return repository.OidcCode{}, err
|
||||||
|
}
|
||||||
|
err = service.DeleteUserinfo(c, oidcCode.Sub)
|
||||||
|
if err != nil {
|
||||||
|
return repository.OidcCode{}, err
|
||||||
|
}
|
||||||
|
return repository.OidcCode{}, ErrCodeExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return oidcCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, sub string) (string, error) {
|
||||||
|
createdAt := time.Now().Unix()
|
||||||
|
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{
|
||||||
|
Algorithm: jose.RS256,
|
||||||
|
Key: service.privateKey,
|
||||||
|
}, &jose.SignerOptions{
|
||||||
|
ExtraHeaders: map[jose.HeaderKey]any{
|
||||||
|
"typ": "jwt",
|
||||||
|
"jku": fmt.Sprintf("%s/.well-known/jwks.json", service.issuer),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := ClaimSet{
|
||||||
|
Iss: service.issuer,
|
||||||
|
Aud: client.ClientID,
|
||||||
|
Sub: sub,
|
||||||
|
Iat: createdAt,
|
||||||
|
Exp: expiresAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(claims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
object, err := signer.Sign(payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := object.CompactSerialize()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, sub string, scope string) (TokenResponse, error) {
|
||||||
|
idToken, err := service.generateIDToken(client, sub)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return TokenResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := rand.Text()
|
||||||
|
refreshToken := rand.Text()
|
||||||
|
|
||||||
|
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
|
// Refresh token lives double the time of an access token but can't be used to access userinfo
|
||||||
|
refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
||||||
|
|
||||||
|
tokenResponse := TokenResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: int64(service.config.SessionExpiry),
|
||||||
|
IDToken: idToken,
|
||||||
|
Scope: strings.ReplaceAll(scope, ",", " "),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = service.queries.CreateOidcToken(c, repository.CreateOidcTokenParams{
|
||||||
|
Sub: sub,
|
||||||
|
AccessTokenHash: service.Hash(accessToken),
|
||||||
|
RefreshTokenHash: service.Hash(refreshToken),
|
||||||
|
ClientID: client.ClientID,
|
||||||
|
Scope: scope,
|
||||||
|
TokenExpiresAt: tokenExpiresAt,
|
||||||
|
RefreshTokenExpiresAt: refrshTokenExpiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return TokenResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken string, reqClientId string) (TokenResponse, error) {
|
||||||
|
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return TokenResponse{}, ErrTokenNotFound
|
||||||
|
}
|
||||||
|
return TokenResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.RefreshTokenExpiresAt < time.Now().Unix() {
|
||||||
|
return TokenResponse{}, ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the client ID in the request matches the client ID in the token
|
||||||
|
if entry.ClientID != reqClientId {
|
||||||
|
return TokenResponse{}, ErrInvalidClient
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := service.generateIDToken(config.OIDCClientConfig{
|
||||||
|
ClientID: entry.ClientID,
|
||||||
|
}, entry.Sub)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return TokenResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := rand.Text()
|
||||||
|
newRefreshToken := rand.Text()
|
||||||
|
|
||||||
|
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
||||||
|
|
||||||
|
tokenResponse := TokenResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: newRefreshToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: int64(service.config.SessionExpiry),
|
||||||
|
IDToken: idToken,
|
||||||
|
Scope: strings.ReplaceAll(entry.Scope, ",", " "),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = service.queries.UpdateOidcTokenByRefreshToken(c, repository.UpdateOidcTokenByRefreshTokenParams{
|
||||||
|
AccessTokenHash: service.Hash(accessToken),
|
||||||
|
RefreshTokenHash: service.Hash(newRefreshToken),
|
||||||
|
TokenExpiresAt: tokenExpiresAt,
|
||||||
|
RefreshTokenExpiresAt: refrshTokenExpiresAt,
|
||||||
|
RefreshTokenHash_2: service.Hash(refreshToken), // that's the selector, it's not stored in the db
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return TokenResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) DeleteCodeEntry(c *gin.Context, codeHash string) error {
|
||||||
|
return service.queries.DeleteOidcCode(c, codeHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) DeleteUserinfo(c *gin.Context, sub string) error {
|
||||||
|
return service.queries.DeleteOidcUserInfo(c, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) DeleteToken(c *gin.Context, tokenHash string) error {
|
||||||
|
return service.queries.DeleteOidcToken(c, tokenHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetAccessToken(c *gin.Context, tokenHash string) (repository.OidcToken, error) {
|
||||||
|
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return repository.OidcToken{}, ErrTokenNotFound
|
||||||
|
}
|
||||||
|
return repository.OidcToken{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.TokenExpiresAt < time.Now().Unix() {
|
||||||
|
// If refresh token is expired, delete the token and userinfo since there is no way for the client to access anything anymore
|
||||||
|
if entry.RefreshTokenExpiresAt < time.Now().Unix() {
|
||||||
|
err := service.DeleteToken(c, tokenHash)
|
||||||
|
if err != nil {
|
||||||
|
return repository.OidcToken{}, err
|
||||||
|
}
|
||||||
|
err = service.DeleteUserinfo(c, entry.Sub)
|
||||||
|
if err != nil {
|
||||||
|
return repository.OidcToken{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repository.OidcToken{}, ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetUserinfo(c *gin.Context, sub string) (repository.OidcUserinfo, error) {
|
||||||
|
return service.queries.GetOidcUserInfo(c, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) CompileUserinfo(user repository.OidcUserinfo, scope string) UserinfoResponse {
|
||||||
|
scopes := strings.Split(scope, ",") // split by comma since it's a db entry
|
||||||
|
userInfo := UserinfoResponse{
|
||||||
|
Sub: user.Sub,
|
||||||
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(scopes, "profile") {
|
||||||
|
userInfo.Name = user.Name
|
||||||
|
userInfo.PreferredUsername = user.PreferredUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(scopes, "email") {
|
||||||
|
userInfo.Email = user.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(scopes, "groups") {
|
||||||
|
if user.Groups != "" {
|
||||||
|
userInfo.Groups = strings.Split(user.Groups, ",")
|
||||||
|
} else {
|
||||||
|
userInfo.Groups = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) Hash(token string) string {
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(token))
|
||||||
|
return fmt.Sprintf("%x", hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) DeleteOldSession(ctx context.Context, sub string) error {
|
||||||
|
err := service.queries.DeleteOidcCodeBySub(ctx, sub)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = service.queries.DeleteOidcTokenBySub(ctx, sub)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = service.queries.DeleteOidcUserInfo(ctx, sub)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup routine - Resource heavy due to the linked tables
|
||||||
|
func (service *OIDCService) Cleanup() {
|
||||||
|
// We need a context for the routine
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(30) * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
|
// For the OIDC tokens, if they are expired we delete the userinfo and codes
|
||||||
|
expiredTokens, err := service.queries.DeleteExpiredOidcTokens(ctx, repository.DeleteExpiredOidcTokensParams{
|
||||||
|
TokenExpiresAt: currentTime,
|
||||||
|
RefreshTokenExpiresAt: currentTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to delete expired tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expiredToken := range expiredTokens {
|
||||||
|
err := service.DeleteOldSession(ctx, expiredToken.Sub)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to delete old session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For expired codes, we need to get the sub, check if tokens are expired and if they are remove everything
|
||||||
|
expiredCodes, err := service.queries.DeleteExpiredOidcCodes(ctx, currentTime)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to delete expired codes")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expiredCode := range expiredCodes {
|
||||||
|
token, err := service.queries.GetOidcTokenBySub(ctx, expiredCode.Sub)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to get OIDC token by sub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.TokenExpiresAt < currentTime && token.RefreshTokenExpiresAt < currentTime {
|
||||||
|
err := service.DeleteOldSession(ctx, expiredCode.Sub)
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Msg("Failed to delete session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetJWK() ([]byte, error) {
|
||||||
|
jwk := jose.JSONWebKey{
|
||||||
|
Key: service.privateKey,
|
||||||
|
Algorithm: string(jose.RS256),
|
||||||
|
Use: "sig",
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwk.Public().MarshalJSON()
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -95,7 +96,7 @@ func IsRedirectSafe(redirectURL string, domain string) bool {
|
|||||||
|
|
||||||
hostname := parsed.Hostname()
|
hostname := parsed.Hostname()
|
||||||
|
|
||||||
if strings.HasSuffix(hostname, domain) {
|
if strings.HasSuffix(hostname, fmt.Sprintf(".%s", domain)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,4 +205,9 @@ func TestIsRedirectSafe(t *testing.T) {
|
|||||||
redirectURL = "http://example.org/page"
|
redirectURL = "http://example.org/page"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.Equal(t, false, result)
|
assert.Equal(t, false, result)
|
||||||
|
|
||||||
|
// Case with malicious domain
|
||||||
|
redirectURL = "https://malicious-example.com/yoyo"
|
||||||
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
|
assert.Equal(t, false, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,3 +49,11 @@ func TestCoalesceToString(t *testing.T) {
|
|||||||
// Test with nil input
|
// Test with nil input
|
||||||
assert.Equal(t, "", utils.CoalesceToString(nil))
|
assert.Equal(t, "", utils.CoalesceToString(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompileUserEmail(t *testing.T) {
|
||||||
|
// Test with valid email
|
||||||
|
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user@example.com", "example.com"))
|
||||||
|
|
||||||
|
// Test with invalid email
|
||||||
|
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user", "example.com"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
@@ -90,3 +92,13 @@ func ParseUser(userStr string) (config.User, error) {
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CompileUserEmail(username string, domain string) string {
|
||||||
|
_, err := mail.ParseAddress(username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("%s@%s", strings.ToLower(username), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
|||||||
113
sql/oidc_queries.sql
Normal file
113
sql/oidc_queries.sql
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
-- name: CreateOidcCode :one
|
||||||
|
INSERT INTO "oidc_codes" (
|
||||||
|
"sub",
|
||||||
|
"code_hash",
|
||||||
|
"scope",
|
||||||
|
"redirect_uri",
|
||||||
|
"client_id",
|
||||||
|
"expires_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetOidcCodeUnsafe :one
|
||||||
|
SELECT * FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?;
|
||||||
|
|
||||||
|
-- name: GetOidcCode :one
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetOidcCodeBySubUnsafe :one
|
||||||
|
SELECT * FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?;
|
||||||
|
|
||||||
|
-- name: GetOidcCodeBySub :one
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteOidcCode :exec
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "code_hash" = ?;
|
||||||
|
|
||||||
|
-- name: DeleteOidcCodeBySub :exec
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "sub" = ?;
|
||||||
|
|
||||||
|
-- name: CreateOidcToken :one
|
||||||
|
INSERT INTO "oidc_tokens" (
|
||||||
|
"sub",
|
||||||
|
"access_token_hash",
|
||||||
|
"refresh_token_hash",
|
||||||
|
"scope",
|
||||||
|
"client_id",
|
||||||
|
"token_expires_at",
|
||||||
|
"refresh_token_expires_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateOidcTokenByRefreshToken :one
|
||||||
|
UPDATE "oidc_tokens" SET
|
||||||
|
"access_token_hash" = ?,
|
||||||
|
"refresh_token_hash" = ?,
|
||||||
|
"token_expires_at" = ?,
|
||||||
|
"refresh_token_expires_at" = ?
|
||||||
|
WHERE "refresh_token_hash" = ?
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetOidcToken :one
|
||||||
|
SELECT * FROM "oidc_tokens"
|
||||||
|
WHERE "access_token_hash" = ?;
|
||||||
|
|
||||||
|
-- name: GetOidcTokenByRefreshToken :one
|
||||||
|
SELECT * FROM "oidc_tokens"
|
||||||
|
WHERE "refresh_token_hash" = ?;
|
||||||
|
|
||||||
|
-- name: GetOidcTokenBySub :one
|
||||||
|
SELECT * FROM "oidc_tokens"
|
||||||
|
WHERE "sub" = ?;
|
||||||
|
|
||||||
|
|
||||||
|
-- name: DeleteOidcToken :exec
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "access_token_hash" = ?;
|
||||||
|
|
||||||
|
-- name: DeleteOidcTokenBySub :exec
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "sub" = ?;
|
||||||
|
|
||||||
|
-- name: CreateOidcUserInfo :one
|
||||||
|
INSERT INTO "oidc_userinfo" (
|
||||||
|
"sub",
|
||||||
|
"name",
|
||||||
|
"preferred_username",
|
||||||
|
"email",
|
||||||
|
"groups",
|
||||||
|
"updated_at"
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetOidcUserInfo :one
|
||||||
|
SELECT * FROM "oidc_userinfo"
|
||||||
|
WHERE "sub" = ?;
|
||||||
|
|
||||||
|
-- name: DeleteOidcUserInfo :exec
|
||||||
|
DELETE FROM "oidc_userinfo"
|
||||||
|
WHERE "sub" = ?;
|
||||||
|
|
||||||
|
-- name: DeleteExpiredOidcCodes :many
|
||||||
|
DELETE FROM "oidc_codes"
|
||||||
|
WHERE "expires_at" < ?
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteExpiredOidcTokens :many
|
||||||
|
DELETE FROM "oidc_tokens"
|
||||||
|
WHERE "token_expires_at" < ? AND "refresh_token_expires_at" < ?
|
||||||
|
RETURNING *;
|
||||||
27
sql/oidc_schemas.sql
Normal file
27
sql/oidc_schemas.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "oidc_codes" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE,
|
||||||
|
"code_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"redirect_uri" TEXT NOT NULL,
|
||||||
|
"client_id" TEXT NOT NULL,
|
||||||
|
"expires_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE,
|
||||||
|
"access_token_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
"refresh_token_hash" TEXT NOT NULL,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"client_id" TEXT NOT NULL,
|
||||||
|
"token_expires_at" INTEGER NOT NULL,
|
||||||
|
"refresh_token_expires_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
||||||
|
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"preferred_username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"groups" TEXT NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
-- name: CreateSession :one
|
-- name: CreateSession :one
|
||||||
INSERT INTO sessions (
|
INSERT INTO "sessions" (
|
||||||
"uuid",
|
"uuid",
|
||||||
"username",
|
"username",
|
||||||
"email",
|
"email",
|
||||||
7
sqlc.yml
7
sqlc.yml
@@ -1,8 +1,8 @@
|
|||||||
version: "2"
|
version: "2"
|
||||||
sql:
|
sql:
|
||||||
- engine: "sqlite"
|
- engine: "sqlite"
|
||||||
queries: "sql/queries.sql"
|
queries: "sql/*_queries.sql"
|
||||||
schema: "sql/schema.sql"
|
schema: "sql/*_schemas.sql"
|
||||||
gen:
|
gen:
|
||||||
go:
|
go:
|
||||||
package: "repository"
|
package: "repository"
|
||||||
@@ -12,6 +12,7 @@ sql:
|
|||||||
oauth_groups: "OAuthGroups"
|
oauth_groups: "OAuthGroups"
|
||||||
oauth_name: "OAuthName"
|
oauth_name: "OAuthName"
|
||||||
oauth_sub: "OAuthSub"
|
oauth_sub: "OAuthSub"
|
||||||
|
redirect_uri: "RedirectURI"
|
||||||
overrides:
|
overrides:
|
||||||
- column: "sessions.oauth_groups"
|
- column: "sessions.oauth_groups"
|
||||||
go_type: "string"
|
go_type: "string"
|
||||||
@@ -19,3 +20,5 @@ sql:
|
|||||||
go_type: "string"
|
go_type: "string"
|
||||||
- column: "sessions.oauth_sub"
|
- column: "sessions.oauth_sub"
|
||||||
go_type: "string"
|
go_type: "string"
|
||||||
|
- column: "sessions.ldap_groups"
|
||||||
|
go_type: "string"
|
||||||
|
|||||||
Reference in New Issue
Block a user