Compare commits

..

17 Commits

Author SHA1 Message Date
Stavros
a1b6ecdd5d fix: further coderabbit suggestions 2025-08-26 14:49:55 +03:00
Stavros
a5e1ae096b fix: coderabbit suggestions 2025-08-26 14:31:09 +03:00
Stavros
d3c40bb366 fix: use correct api path in login mutation 2025-08-26 13:21:36 +03:00
Stavros
8435cbe434 fix: configure middlewares before groups 2025-08-26 13:17:10 +03:00
Stavros
77296daef3 feat: add some logging 2025-08-26 12:45:24 +03:00
Stavros
645c555cf0 refactor: move resource handling to a controller 2025-08-26 12:22:10 +03:00
Stavros
03af18fd15 fix: validate resource file paths in ui middleware 2025-08-25 22:31:57 +03:00
Stavros
cb8022af91 fix: fix typo in ui middleware 2025-08-25 22:25:17 +03:00
Stavros
304c920b7b feat: allow customizability of resources dir 2025-08-25 22:20:00 +03:00
Stavros
6418cbe2ba refactor: use more clear name for frontend assets 2025-08-25 22:16:26 +03:00
Stavros
04213836a1 refactor: split utils into smaller files 2025-08-25 22:13:53 +03:00
Stavros
659d3561e0 refactor: use a boostrap service to bootstrap the app 2025-08-25 22:03:06 +03:00
Stavros
dbadb096b4 feat: create oauth broker service 2025-08-25 19:33:52 +03:00
Stavros
44f35af3bf refactor: move oauth providers into services (non-working) 2025-08-25 17:50:34 +03:00
Stavros
dfdc656145 refactor: use controller approach in handlers 2025-08-25 16:40:06 +03:00
Stavros
e1d8ce3cb5 refactor: use context fom middleware in handlers 2025-08-25 14:19:52 +03:00
Stavros
ace22acdb2 wip: add middlewares 2025-08-25 13:28:30 +03:00
78 changed files with 1235 additions and 3636 deletions

View File

@@ -1,9 +1,25 @@
PORT=3000 PORT=3000
ADDRESS=0.0.0.0 ADDRESS=0.0.0.0
SECRET=app_secret
SECRET_FILE=app_secret_file
APP_URL=http://localhost:3000 APP_URL=http://localhost:3000
USERS=your_user_password_hash USERS=your_user_password_hash
USERS_FILE=users_file USERS_FILE=users_file
SECURE_COOKIE=false SECURE_COOKIE=false
GITHUB_CLIENT_ID=github_client_id
GITHUB_CLIENT_SECRET=github_client_secret
GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
GENERIC_CLIENT_ID=generic_client_id
GENERIC_CLIENT_SECRET=generic_client_secret
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
GENERIC_SCOPES=generic_scopes
GENERIC_AUTH_URL=generic_auth_url
GENERIC_TOKEN_URL=generic_token_url
GENERIC_USER_URL=generic_user_url
DISABLE_CONTINUE=false
OAUTH_WHITELIST= OAUTH_WHITELIST=
GENERIC_NAME=My OAuth GENERIC_NAME=My OAuth
SESSION_EXPIRY=7200 SESSION_EXPIRY=7200
@@ -14,9 +30,4 @@ APP_TITLE=Tinyauth SSO
FORGOT_PASSWORD_MESSAGE=Some message about resetting the password FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
OAUTH_AUTO_REDIRECT=none OAUTH_AUTO_REDIRECT=none
BACKGROUND_IMAGE=some_image_url BACKGROUND_IMAGE=some_image_url
GENERIC_SKIP_SSL=false GENERIC_SKIP_SSL=false
RESOURCES_DIR=/data/resources
DATABASE_PATH=/data/tinyauth.db
DISABLE_ANALYTICS=false
DISABLE_RESOURCES=false
TRUSTED_PROXIES=

View File

@@ -80,7 +80,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 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
@@ -126,7 +126,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 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0

View File

@@ -58,7 +58,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 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
@@ -101,7 +101,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 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0

View File

@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM oven/bun:1.2.23-alpine AS frontend-builder FROM oven/bun:1.2.20-alpine AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -38,7 +38,7 @@ 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}" RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${VERSION} -X tinyauth/internal/constants.CommitHash=${COMMIT_HASH} -X tinyauth/internal/constants.BuildTimestamp=${BUILD_TIMESTAMP}"
# Runner # Runner
FROM alpine:3.22 AS runner FROM alpine:3.22 AS runner
@@ -51,6 +51,4 @@ COPY --from=builder /tinyauth/tinyauth ./
EXPOSE 3000 EXPOSE 3000
VOLUME ["/data"]
ENTRYPOINT ["./tinyauth"] ENTRYPOINT ["./tinyauth"]

View File

@@ -27,6 +27,12 @@ var rootCmd = &cobra.Command{
log.Fatal().Err(err).Msg("Failed to parse config") log.Fatal().Err(err).Msg("Failed to parse config")
} }
// Check if secrets have a file associated with them
conf.Secret = utils.GetSecret(conf.Secret, conf.SecretFile)
conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile)
conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile)
conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile)
// Validate config // Validate config
v := validator.New() v := validator.New()
@@ -52,7 +58,6 @@ var rootCmd = &cobra.Command{
} }
func Execute() { func Execute() {
rootCmd.FParseErrWhitelist.UnknownFlags = true
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Failed to execute command") log.Fatal().Err(err).Msg("Failed to execute command")
@@ -72,10 +77,28 @@ func init() {
}{ }{
{"port", 3000, "Port to run the server on."}, {"port", 3000, "Port to run the server on."},
{"address", "0.0.0.0", "Address to bind the server to."}, {"address", "0.0.0.0", "Address to bind the server to."},
{"secret", "", "Secret to use for the cookie."},
{"secret-file", "", "Path to a file containing the secret."},
{"app-url", "", "The Tinyauth URL."}, {"app-url", "", "The Tinyauth URL."},
{"users", "", "Comma separated list of users in the format username:hash."}, {"users", "", "Comma separated list of users in the format username:hash."},
{"users-file", "", "Path to a file containing users in the format username:hash."}, {"users-file", "", "Path to a file containing users in the format username:hash."},
{"secure-cookie", false, "Send cookie over secure connection only."}, {"secure-cookie", false, "Send cookie over secure connection only."},
{"github-client-id", "", "Github OAuth client ID."},
{"github-client-secret", "", "Github OAuth client secret."},
{"github-client-secret-file", "", "Github OAuth client secret file."},
{"google-client-id", "", "Google OAuth client ID."},
{"google-client-secret", "", "Google OAuth client secret."},
{"google-client-secret-file", "", "Google OAuth client secret file."},
{"generic-client-id", "", "Generic OAuth client ID."},
{"generic-client-secret", "", "Generic OAuth client secret."},
{"generic-client-secret-file", "", "Generic OAuth client secret file."},
{"generic-scopes", "", "Generic OAuth scopes."},
{"generic-auth-url", "", "Generic OAuth auth URL."},
{"generic-token-url", "", "Generic OAuth token URL."},
{"generic-user-url", "", "Generic OAuth user info URL."},
{"generic-name", "Generic", "Generic OAuth provider name."},
{"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."},
{"disable-continue", false, "Disable continue screen and redirect to app directly."},
{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, {"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."},
{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, {"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"},
{"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, {"session-expiry", 86400, "Session (cookie) expiration time in seconds."},
@@ -92,10 +115,6 @@ func init() {
{"ldap-insecure", false, "Skip certificate verification for the LDAP server."}, {"ldap-insecure", false, "Skip certificate verification for the LDAP server."},
{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."},
{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."},
{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."},
{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."},
{"disable-analytics", false, "Disable anonymous version collection."},
{"disable-resources", false, "Disable the resources server."},
} }
for _, opt := range configOptions { for _, opt := range configOptions {

View File

@@ -70,7 +70,7 @@ var VerifyCmd = &cobra.Command{
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
if err != nil { if err != nil {
log.Fatal().Msg("Password is incorrect") log.Fatal().Msg("Ppassword is incorrect")
} }
if user.TotpSecret == "" { if user.TotpSecret == "" {

View File

@@ -34,17 +34,12 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
args:
- VERSION=development
- COMMIT_HASH=development
- BUILD_TIMESTAMP=000-00-00T00:00:00Z
env_file: .env env_file: .env
volumes: volumes:
- ./internal:/tinyauth/internal - ./internal:/tinyauth/internal
- ./cmd:/tinyauth/cmd - ./cmd:/tinyauth/cmd
- ./main.go:/tinyauth/main.go - ./main.go:/tinyauth/main.go
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
ports: ports:
- 3000:3000 - 3000:3000
- 4000:4000 - 4000:4000

View File

@@ -20,10 +20,9 @@ services:
container_name: tinyauth container_name: tinyauth
image: ghcr.io/steveiliop56/tinyauth:v3 image: ghcr.io/steveiliop56/tinyauth:v3
environment: environment:
- SECRET=some-random-32-chars-string
- APP_URL=https://tinyauth.example.com - APP_URL=https://tinyauth.example.com
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password - USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
volumes:
- ./data:/data
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)

View File

@@ -4,60 +4,63 @@
"": { "": {
"name": "tinyauth-shadcn", "name": "tinyauth-shadcn",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.1",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.84.1",
"axios": "^1.12.2", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^25.5.2", "dompurify": "^3.2.6",
"i18next": "^25.3.2",
"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.544.0", "lucide-react": "^0.539.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.62.0",
"react-i18next": "^15.7.3", "react-i18next": "^15.6.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.9.1", "react-router": "^7.8.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.11",
"zod": "^4.1.9", "zod": "^4.0.15",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.35.0", "@eslint/js": "^9.32.0",
"@tanstack/eslint-plugin-query": "^5.89.0", "@tanstack/eslint-plugin-query": "^5.83.1",
"@types/node": "^24.5.2", "@types/node": "^24.2.0",
"@types/react": "^19.1.13", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.3", "@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.35.0", "eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.4.0", "globals": "^16.3.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.3.6",
"typescript": "~5.9.2", "typescript": "~5.9.2",
"typescript-eslint": "^8.44.0", "typescript-eslint": "^8.39.0",
"vite": "^7.1.6", "vite": "^7.1.1",
}, },
}, },
}, },
"packages": { "packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="], "@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "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-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@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/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
"@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.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=="],
@@ -65,7 +68,7 @@
"@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.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.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.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
"@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=="],
@@ -75,9 +78,9 @@
"@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.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
"@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.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
@@ -87,9 +90,9 @@
"@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.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
"@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/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -141,23 +144,23 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="],
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], "@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="], "@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="],
"@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="],
@@ -167,7 +170,7 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], "@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -179,17 +182,15 @@
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@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.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@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.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -199,7 +200,7 @@
"@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.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
@@ -211,9 +212,9 @@
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
@@ -221,13 +222,13 @@
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
@@ -253,7 +254,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.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.30", "", {}, "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw=="],
"@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=="],
@@ -297,41 +298,41 @@
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.89.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-vz8TEuw9GO0xXIdreMpcofvOY17T3cjgob9bSFln8yQsKsbsUvtpvV3F8pVC3tZEDq0IwO++3/e0/+7YKEarNA=="], "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.83.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg=="],
"@tanstack/query-core": ["@tanstack/query-core@5.89.0", "", {}, "sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q=="], "@tanstack/query-core": ["@tanstack/query-core@5.83.1", "", {}, "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q=="],
"@tanstack/react-query": ["@tanstack/react-query@5.89.0", "", { "dependencies": { "@tanstack/query-core": "5.89.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A=="], "@tanstack/react-query": ["@tanstack/react-query@5.84.1", "", { "dependencies": { "@tanstack/query-core": "5.83.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw=="],
"@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=="],
@@ -343,7 +344,7 @@
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -355,37 +356,39 @@
"@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@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], "@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@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.44.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/type-utils": "8.44.0", "@typescript-eslint/utils": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.0", "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.0", "@typescript-eslint/tsconfig-utils": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="],
"@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.0.3", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.35", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.30", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw=="],
"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=="],
@@ -401,7 +404,7 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="], "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
@@ -471,11 +474,13 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="], "electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -491,7 +496,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="], "eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
@@ -525,7 +530,7 @@
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "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.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"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=="],
@@ -555,7 +560,7 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
@@ -579,7 +584,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.5.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw=="], "i18next": ["i18next@25.3.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA=="],
"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=="],
@@ -663,9 +668,9 @@
"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.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], "lucide-react": ["lucide-react@0.539.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg=="],
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -791,7 +796,7 @@
"react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="], "react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="],
"react-i18next": ["react-i18next@15.7.3", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw=="], "react-i18next": ["react-i18next@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg=="],
"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=="],
@@ -801,7 +806,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.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="], "react-router": ["react-router@7.8.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg=="],
"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=="],
@@ -845,13 +850,13 @@
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
"tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@@ -863,15 +868,15 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="], "tw-animate-css": ["tw-animate-css@1.3.6", "", {}, "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript-eslint": ["typescript-eslint@8.44.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.0", "@typescript-eslint/parser": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/utils": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw=="], "typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
@@ -897,7 +902,7 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
"vite": ["vite@7.1.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="], "vite": ["vite@7.1.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
@@ -909,19 +914,21 @@
"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.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], "zod": ["zod@4.0.15", "", {}, "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], "@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
"@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], "@babel/generator/@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=="],
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="], "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="],
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], "@babel/helper-module-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/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
"@babel/helpers/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+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.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
@@ -935,21 +942,15 @@
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
"@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -965,40 +966,36 @@
"@types/babel__traverse/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], "@types/babel__traverse/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="], "i18next-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=="],
@@ -1009,7 +1006,11 @@
"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.44.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg=="], "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="],
"@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/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=="],
@@ -1017,44 +1018,52 @@
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], "@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/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/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
"@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=="], "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
"@babel/helper-module-transforms/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@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.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="],
"@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.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"@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/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/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.0", "", { "dependencies": { "@typescript-eslint/types": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0" } }, "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA=="], "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.0", "", {}, "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA=="], "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@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=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
} }
} }

View File

@@ -8,7 +8,6 @@
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Tinyauth" /> <meta name="apple-mobile-web-app-title" content="Tinyauth" />
<meta name="robots" content="noindex" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<title>Tinyauth</title> <title>Tinyauth</title>
</head> </head>

View File

@@ -10,48 +10,49 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.1",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.84.1",
"axios": "^1.12.2", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^25.5.2", "dompurify": "^3.2.6",
"i18next": "^25.3.2",
"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.544.0", "lucide-react": "^0.539.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.62.0",
"react-i18next": "^15.7.3", "react-i18next": "^15.6.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.9.1", "react-router": "^7.8.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.11",
"zod": "^4.1.9" "zod": "^4.0.15"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.35.0", "@eslint/js": "^9.32.0",
"@tanstack/eslint-plugin-query": "^5.89.0", "@tanstack/eslint-plugin-query": "^5.83.1",
"@types/node": "^24.5.2", "@types/node": "^24.2.0",
"@types/react": "^19.1.13", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.3", "@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.35.0", "eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.4.0", "globals": "^16.3.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.3.6",
"typescript": "~5.9.2", "typescript": "~5.9.2",
"typescript-eslint": "^8.44.0", "typescript-eslint": "^8.39.0",
"vite": "^7.1.6" "vite": "^7.1.1"
} }
} }

View File

@@ -5,8 +5,8 @@ export const App = () => {
const { isLoggedIn } = useUserContext(); const { isLoggedIn } = useUserContext();
if (isLoggedIn) { if (isLoggedIn) {
return <Navigate to="/logout" replace />; return <Navigate to="/logout" />;
} }
return <Navigate to="/login" replace />; return <Navigate to="/login" />;
}; };

View File

@@ -44,7 +44,6 @@ export const TotpForm = (props: Props) => {
disabled={loading} disabled={loading}
{...field} {...field}
autoComplete="one-time-code" autoComplete="one-time-code"
autoFocus
> >
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot index={0} /> <InputOTPSlot index={0} />

View File

@@ -1,56 +0,0 @@
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
import { Button } from "../ui/button";
import { Trans, useTranslation } from "react-i18next";
import { useLocation } from "react-router";
interface Props {
onClick: () => void;
appUrl: string;
currentUrl: string;
}
export const DomainWarning = (props: Props) => {
const { onClick, appUrl, currentUrl } = props;
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("domainWarningTitle")}</CardTitle>
<CardDescription>
<Trans
t={t}
i18nKey="domainWarningSubtitle"
values={{ appUrl, currentUrl }}
components={{ code: <code /> }}
/>
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button onClick={onClick} variant="warning">
{t("ignoreTitle")}
</Button>
<Button
onClick={() =>
window.location.assign(
`${appUrl}/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`,
)
}
variant="outline"
>
{t("goToCorrectDomainTitle")}
</Button>
</CardFooter>
</Card>
);
};

View File

@@ -1,6 +1,6 @@
import type { SVGProps } from "react"; import type { SVGProps } from "react";
export function OAuthIcon(props: SVGProps<SVGSVGElement>) { export function GenericIcon(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,18 +0,0 @@
import type { SVGProps } from "react";
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="2em"
height="2em"
viewBox="0 0 256 256"
{...props}
>
<path fill="#f1511b" d="M121.666 121.666H0V0h121.666z"></path>
<path fill="#80cc28" d="M256 121.666H134.335V0H256z"></path>
<path fill="#00adef" d="M121.663 256.002H0V134.336h121.663z"></path>
<path fill="#fbbc09" d="M256 256.002H134.335V134.336H256z"></path>
</svg>
);
}

View File

@@ -1,20 +0,0 @@
import type { SVGProps } from "react";
export function PocketIDIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width={512}
height={512}
viewBox="0 0 512 512"
{...props}
>
<circle cx="256" cy="256" r="256" />
<path
d="M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z"
className="fill-white"
/>
</svg>
);
}

View File

@@ -1,26 +0,0 @@
import type { SVGProps } from "react";
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width={512}
height={512}
viewBox="0 0 512 512"
{...props}
>
<path
className="opacity-80"
fill="currentColor"
d="M65.6 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9S1.8 219 1.8 254.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"
/>
<path
d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.3 0 63.9-28.6 63.9-63.9S481.6 0 446.4 0c-35.3 0-63.9 28.6-63.9 63.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"
className="opacity-20"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -1,10 +1,8 @@
import { useAppContext } from "@/context/app-context"; import { useAppContext } from "@/context/app-context";
import { LanguageSelector } from "../language/language"; import { LanguageSelector } from "../language/language";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { useCallback, useState } from "react";
import { DomainWarning } from "../domain-warning/domain-warning";
const BaseLayout = ({ children }: { children: React.ReactNode }) => { export const Layout = () => {
const { backgroundImage } = useAppContext(); const { backgroundImage } = useAppContext();
return ( return (
@@ -17,38 +15,7 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
}} }}
> >
<LanguageSelector /> <LanguageSelector />
{children} <Outlet />
</div> </div>
); );
}; };
export const Layout = () => {
const { appUrl } = useAppContext();
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
});
const currentUrl = window.location.origin;
const handleIgnore = useCallback(() => {
window.sessionStorage.setItem("ignoreDomainWarning", "true");
setIgnoreDomainWarning(true);
}, [setIgnoreDomainWarning]);
if (!ignoreDomainWarning && appUrl !== currentUrl) {
return (
<BaseLayout>
<DomainWarning
appUrl={appUrl}
currentUrl={currentUrl}
onClick={() => handleIgnore()}
/>
</BaseLayout>
);
}
return (
<BaseLayout>
<Outlet />
</BaseLayout>
);
};

View File

@@ -22,7 +22,7 @@ const buttonVariants = cva(
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
warning: warning:
"bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40", "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@@ -156,7 +156,7 @@ ul {
} }
code { 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;
} }
.lead { .lead {

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> 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", "continueTitle": "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?", "continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out", "logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again", "logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out", "logoutSuccessTitle": "Logged out",
@@ -47,6 +44,8 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again",
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
"cancelTitle": "Cancel", "cancelTitle": "Cancel",
"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.",
@@ -54,9 +53,5 @@
"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",
"invalidInput": "Invalid input", "invalidInput": "Invalid input"
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
} }

View File

@@ -14,17 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Insecure redirect", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> 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", "continueTitle": "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?", "continueSubtitle": "Click the button to continue to your app.",
"logoutFailTitle": "Failed to log out", "logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again", "logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out", "logoutSuccessTitle": "Logged out",
@@ -47,6 +44,8 @@
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again", "unauthorizedButton": "Try again",
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
"cancelTitle": "Cancel", "cancelTitle": "Cancel",
"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.",
@@ -54,9 +53,5 @@
"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",
"invalidInput": "Invalid input", "invalidInput": "Invalid input"
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
} }

View File

@@ -11,101 +11,60 @@ import { useUserContext } from "@/context/user-context";
import { isValidUrl } from "@/lib/utils"; 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 DOMPurify from "dompurify";
import { useState } from "react";
export const ContinuePage = () => { export const ContinuePage = () => {
const { cookieDomain } = useAppContext();
const { isLoggedIn } = useUserContext(); const { isLoggedIn } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
const { domain, disableContinue } = useAppContext();
const { search } = useLocation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showRedirectButton, setShowRedirectButton] = useState(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 = if (!redirectURI) {
redirectUri !== null ? isValidUrl(redirectUri) : false; return <Navigate to="/logout" />;
const redirectUriObj = isValidRedirectUri }
? new URL(redirectUri as string)
: null; if (!isValidUrl(DOMPurify.sanitize(redirectURI))) {
const isTrustedRedirectUri = return <Navigate to="/logout" />;
redirectUriObj !== null }
? redirectUriObj.hostname === cookieDomain ||
redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
: false;
const isAllowedRedirectProto =
redirectUriObj !== null
? redirectUriObj.protocol === "https:" ||
redirectUriObj.protocol === "http:"
: false;
const isHttpsDowngrade =
redirectUriObj !== null
? redirectUriObj.protocol === "http:" &&
window.location.protocol === "https:"
: false;
const handleRedirect = () => { const handleRedirect = () => {
setLoading(true); setLoading(true);
window.location.assign(redirectUriObj!.toString()); window.location.href = DOMPurify.sanitize(redirectURI);
};
useEffect(() => {
if (
!isLoggedIn ||
!isValidRedirectUri ||
!isTrustedRedirectUri ||
!isAllowedRedirectProto ||
isHttpsDowngrade
) {
return;
}
const auto = setTimeout(() => {
handleRedirect();
}, 100);
const reveal = setTimeout(() => {
setLoading(false);
setShowRedirectButton(true);
}, 5000);
return () => {
clearTimeout(auto);
clearTimeout(reveal);
};
}, []);
if (!isLoggedIn) {
return (
<Navigate
to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`}
replace
/>
);
} }
if (!isValidRedirectUri || !isAllowedRedirectProto) { if (disableContinue) {
return <Navigate to="/logout" replace />; handleRedirect();
} }
if (!isTrustedRedirectUri) { const { t } = useTranslation();
const navigate = useNavigate();
const url = new URL(redirectURI);
if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) {
return ( return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>
<CardTitle className="text-3xl"> <CardTitle className="text-3xl">
{t("continueUntrustedRedirectTitle")} {t("untrustedRedirectTitle")}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
<Trans <Trans
i18nKey="continueUntrustedRedirectSubtitle" i18nKey="untrustedRedirectSubtitle"
t={t} t={t}
components={{ components={{
code: <code />, code: <code />,
}} }}
values={{ cookieDomain }} values={{ domain }}
/> />
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@@ -117,11 +76,7 @@ export const ContinuePage = () => {
> >
{t("continueTitle")} {t("continueTitle")}
</Button> </Button>
<Button <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
onClick={() => navigate("/logout")}
variant="outline"
disabled={loading}
>
{t("cancelTitle")} {t("cancelTitle")}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -129,9 +84,9 @@ export const ContinuePage = () => {
); );
} }
if (isHttpsDowngrade) { if (url.protocol === "http:" && window.location.protocol === "https:") {
return ( return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>
<CardTitle className="text-3xl"> <CardTitle className="text-3xl">
{t("continueInsecureRedirectTitle")} {t("continueInsecureRedirectTitle")}
@@ -147,14 +102,14 @@ 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={loading}
variant="warning"
>
{t("continueTitle")} {t("continueTitle")}
</Button> </Button>
<Button <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
onClick={() => navigate("/logout")}
variant="outline"
disabled={loading}
>
{t("cancelTitle")} {t("cancelTitle")}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -165,18 +120,17 @@ export const ContinuePage = () => {
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"> <CardTitle className="text-3xl">{t("continueTitle")}</CardTitle>
{t("continueRedirectingTitle")} <CardDescription>{t("continueSubtitle")}</CardDescription>
</CardTitle>
<CardDescription>{t("continueRedirectingSubtitle")}</CardDescription>
</CardHeader> </CardHeader>
{showRedirectButton && ( <CardFooter className="flex flex-col items-stretch">
<CardFooter className="flex flex-col items-stretch"> <Button
<Button onClick={handleRedirect}> onClick={handleRedirect}
{t("continueRedirectManually")} loading={loading}
</Button> >
</CardFooter> {t("continueTitle")}
)} </Button>
</CardFooter>
</Card> </Card>
); );
}; };

View File

@@ -1,18 +1,13 @@
import { LoginForm } from "@/components/auth/login-form"; import { LoginForm } from "@/components/auth/login-form";
import { GenericIcon } from "@/components/icons/generic";
import { GithubIcon } from "@/components/icons/github"; import { GithubIcon } from "@/components/icons/github";
import { GoogleIcon } from "@/components/icons/google"; import { GoogleIcon } from "@/components/icons/google";
import { MicrosoftIcon } from "@/components/icons/microsoft";
import { OAuthIcon } from "@/components/icons/oauth";
import { PocketIDIcon } from "@/components/icons/pocket-id";
import { TailscaleIcon } from "@/components/icons/tailscale";
import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription, CardDescription,
CardContent, CardContent,
CardFooter,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { OAuthButton } from "@/components/ui/oauth-button"; import { OAuthButton } from "@/components/ui/oauth-button";
import { SeperatorWithChildren } from "@/components/ui/separator"; import { SeperatorWithChildren } from "@/components/ui/separator";
@@ -22,40 +17,30 @@ import { useIsMounted } from "@/lib/hooks/use-is-mounted";
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";
import { useEffect, useRef, useState } from "react"; import { useEffect } 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";
const iconMap: Record<string, React.ReactNode> = {
google: <GoogleIcon />,
github: <GithubIcon />,
tailscale: <TailscaleIcon />,
microsoft: <MicrosoftIcon />,
pocketid: <PocketIDIcon />,
};
export const LoginPage = () => { export const LoginPage = () => {
const { isLoggedIn } = useUserContext(); const { isLoggedIn } = useUserContext();
const { providers, title, oauthAutoRedirect } = useAppContext();
if (isLoggedIn) {
return <Navigate to="/logout" />;
}
const { configuredProviders, title, oauthAutoRedirect, genericName } = useAppContext();
const { search } = useLocation(); const { search } = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] =
useState(false);
const [showRedirectButton, setShowRedirectButton] = useState(false);
const redirectTimer = useRef<number | null>(null);
const redirectButtonTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri"); const redirectUri = searchParams.get("redirect_uri");
const oauthProviders = providers.filter( const oauthConfigured =
(provider) => provider.id !== "username", configuredProviders.filter((provider) => provider !== "username").length >
); 0;
const userAuthConfigured = const userAuthConfigured = configuredProviders.includes("username");
providers.find((provider) => provider.id === "username") !== undefined;
const oauthMutation = useMutation({ const oauthMutation = useMutation({
mutationFn: (provider: string) => mutationFn: (provider: string) =>
@@ -68,12 +53,11 @@ export const LoginPage = () => {
description: t("loginOauthSuccessSubtitle"), description: t("loginOauthSuccessSubtitle"),
}); });
redirectTimer.current = window.setTimeout(() => { setTimeout(() => {
window.location.replace(data.data.url); window.location.href = data.data.url;
}, 500); }, 500);
}, },
onError: () => { onError: () => {
setOauthAutoRedirectHandover(false);
toast.error(t("loginOauthFailTitle"), { toast.error(t("loginOauthFailTitle"), {
description: t("loginOauthFailSubtitle"), description: t("loginOauthFailSubtitle"),
}); });
@@ -95,7 +79,7 @@ export const LoginPage = () => {
description: t("loginSuccessSubtitle"), description: t("loginSuccessSubtitle"),
}); });
redirectTimer.current = window.setTimeout(() => { setTimeout(() => {
window.location.replace( window.location.replace(
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
); );
@@ -114,99 +98,61 @@ export const LoginPage = () => {
useEffect(() => { useEffect(() => {
if (isMounted()) { if (isMounted()) {
if ( if (
oauthProviders.length !== 0 && oauthConfigured &&
providers.find((provider) => provider.id === oauthAutoRedirect) && configuredProviders.includes(oauthAutoRedirect) &&
!isLoggedIn &&
redirectUri redirectUri
) { ) {
setOauthAutoRedirectHandover(true);
oauthMutation.mutate(oauthAutoRedirect); oauthMutation.mutate(oauthAutoRedirect);
redirectButtonTimer.current = window.setTimeout(() => {
setShowRedirectButton(true);
}, 5000);
} }
} }
}, []); }, []);
useEffect(
() => () => {
if (redirectTimer.current) clearTimeout(redirectTimer.current);
if (redirectButtonTimer.current)
clearTimeout(redirectButtonTimer.current);
},
[],
);
if (isLoggedIn && redirectUri) {
return (
<Navigate
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
replace
/>
);
}
if (isLoggedIn) {
return <Navigate to="/logout" replace />;
}
if (oauthAutoRedirectHandover) {
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("loginOauthAutoRedirectTitle")}
</CardTitle>
<CardDescription>
{t("loginOauthAutoRedirectSubtitle")}
</CardDescription>
</CardHeader>
{showRedirectButton && (
<CardFooter className="flex flex-col items-stretch">
<Button
onClick={() => {
window.location.replace(oauthMutation.data?.data.url);
}}
>
{t("loginOauthAutoRedirectButton")}
</Button>
</CardFooter>
)}
</Card>
);
}
return ( return (
<Card className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>
<CardTitle className="text-center text-3xl">{title}</CardTitle> <CardTitle className="text-center text-3xl">{title}</CardTitle>
{providers.length > 0 && ( {configuredProviders.length > 0 && (
<CardDescription className="text-center"> <CardDescription className="text-center">
{oauthProviders.length !== 0 {oauthConfigured ? t("loginTitle") : t("loginTitleSimple")}
? t("loginTitle")
: t("loginTitleSimple")}
</CardDescription> </CardDescription>
)} )}
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{oauthProviders.length !== 0 && ( {oauthConfigured && (
<div className="flex flex-col gap-2 items-center justify-center"> <div className="flex flex-col gap-2 items-center justify-center">
{oauthProviders.map((provider) => ( {configuredProviders.includes("google") && (
<OAuthButton <OAuthButton
key={provider.id} title="Google"
title={provider.name} icon={<GoogleIcon />}
icon={iconMap[provider.id] ?? <OAuthIcon />}
className="w-full" className="w-full"
onClick={() => oauthMutation.mutate(provider.id)} onClick={() => oauthMutation.mutate("google")}
loading={ loading={oauthMutation.isPending && oauthMutation.variables === "google"}
oauthMutation.isPending &&
oauthMutation.variables === provider.id
}
disabled={oauthMutation.isPending || loginMutation.isPending} disabled={oauthMutation.isPending || loginMutation.isPending}
/> />
))} )}
{configuredProviders.includes("github") && (
<OAuthButton
title="Github"
icon={<GithubIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("github")}
loading={oauthMutation.isPending && oauthMutation.variables === "github"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
{configuredProviders.includes("generic") && (
<OAuthButton
title={genericName}
icon={<GenericIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("generic")}
loading={oauthMutation.isPending && oauthMutation.variables === "generic"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
</div> </div>
)} )}
{userAuthConfigured && oauthProviders.length !== 0 && ( {userAuthConfigured && oauthConfigured && (
<SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren> <SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
)} )}
{userAuthConfigured && ( {userAuthConfigured && (
@@ -215,7 +161,7 @@ export const LoginPage = () => {
loading={loginMutation.isPending || oauthMutation.isPending} loading={loginMutation.isPending || oauthMutation.isPending}
/> />
)} )}
{providers.length == 0 && ( {configuredProviders.length == 0 && (
<p className="text-center text-red-600 max-w-sm"> <p className="text-center text-red-600 max-w-sm">
{t("failedToFetchProvidersTitle")} {t("failedToFetchProvidersTitle")}
</p> </p>

View File

@@ -6,19 +6,24 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context"; import { useUserContext } from "@/context/user-context";
import { capitalize } from "@/lib/utils";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { useEffect, useRef } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
export const LogoutPage = () => { export const LogoutPage = () => {
const { provider, username, isLoggedIn, email, oauthName } = useUserContext(); const { provider, username, isLoggedIn, email } = useUserContext();
const { t } = useTranslation();
const redirectTimer = useRef<number | null>(null); if (!isLoggedIn) {
return <Navigate to="/login" />;
}
const { genericName } = useAppContext();
const { t } = useTranslation();
const logoutMutation = useMutation({ const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"), mutationFn: () => axios.post("/api/user/logout"),
@@ -28,8 +33,8 @@ export const LogoutPage = () => {
description: t("logoutSuccessSubtitle"), description: t("logoutSuccessSubtitle"),
}); });
redirectTimer.current = window.setTimeout(() => { setTimeout(async () => {
window.location.assign("/login"); window.location.replace("/login");
}, 500); }, 500);
}, },
onError: () => { onError: () => {
@@ -39,17 +44,6 @@ export const LogoutPage = () => {
}, },
}); });
useEffect(
() => () => {
if (redirectTimer.current) clearTimeout(redirectTimer.current);
},
[],
);
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}
return ( return (
<Card className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>
@@ -64,7 +58,8 @@ export const LogoutPage = () => {
}} }}
values={{ values={{
username: email, username: email,
provider: oauthName, provider:
provider === "generic" ? genericName : capitalize(provider),
}} }}
/> />
) : ( ) : (

View File

@@ -12,19 +12,22 @@ import { useUserContext } from "@/context/user-context";
import { TotpSchema } from "@/schemas/totp-schema"; import { TotpSchema } from "@/schemas/totp-schema";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { useEffect, useId, useRef } from "react"; import { useId } 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";
export const TotpPage = () => { export const TotpPage = () => {
const { totpPending } = useUserContext(); const { totpPending } = useUserContext();
if (!totpPending) {
return <Navigate to="/" />;
}
const { t } = useTranslation(); const { t } = useTranslation();
const { search } = useLocation(); const { search } = useLocation();
const formId = useId(); const formId = useId();
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri"); const redirectUri = searchParams.get("redirect_uri");
@@ -36,7 +39,7 @@ export const TotpPage = () => {
description: t("totpSuccessSubtitle"), description: t("totpSuccessSubtitle"),
}); });
redirectTimer.current = window.setTimeout(() => { setTimeout(() => {
window.location.replace( window.location.replace(
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
); );
@@ -49,17 +52,6 @@ export const TotpPage = () => {
}, },
}); });
useEffect(
() => () => {
if (redirectTimer.current) clearTimeout(redirectTimer.current);
},
[],
);
if (!totpPending) {
return <Navigate to="/" replace />;
}
return ( return (
<Card className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>

View File

@@ -12,10 +12,6 @@ import { Navigate, useLocation, useNavigate } from "react-router";
export const UnauthorizedPage = () => { export const UnauthorizedPage = () => {
const { search } = useLocation(); const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const username = searchParams.get("username"); const username = searchParams.get("username");
@@ -23,15 +19,19 @@ export const UnauthorizedPage = () => {
const groupErr = searchParams.get("groupErr"); const groupErr = searchParams.get("groupErr");
const ip = searchParams.get("ip"); const ip = searchParams.get("ip");
if (!username && !ip) {
return <Navigate to="/" />;
}
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const handleRedirect = () => { const handleRedirect = () => {
setLoading(true); setLoading(true);
navigate("/login"); navigate("/login");
}; };
if (!username && !ip) {
return <Navigate to="/" />;
}
let i18nKey = "unauthorizedLoginSubtitle"; let i18nKey = "unauthorizedLoginSubtitle";
if (resource) { if (resource) {

View File

@@ -1,19 +1,14 @@
import { z } from "zod"; import { z } from "zod";
export const providerSchema = z.object({
id: z.string(),
name: z.string(),
oauth: z.boolean(),
});
export const appContextSchema = z.object({ export const appContextSchema = z.object({
providers: z.array(providerSchema), configuredProviders: z.array(z.string()),
disableContinue: z.boolean(),
title: z.string(), title: z.string(),
appUrl: z.string(), genericName: z.string(),
cookieDomain: z.string(), domain: z.string(),
forgotPasswordMessage: z.string(), forgotPasswordMessage: z.string(),
oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]),
backgroundImage: z.string(), backgroundImage: z.string(),
oauthAutoRedirect: z.string(),
}); });
export type AppContextSchema = z.infer<typeof appContextSchema>; export type AppContextSchema = z.infer<typeof appContextSchema>;

View File

@@ -8,7 +8,6 @@ export const userContextSchema = z.object({
provider: z.string(), provider: z.string(),
oauth: z.boolean(), oauth: z.boolean(),
totpPending: z.boolean(), totpPending: z.boolean(),
oauthName: z.string(),
}); });
export type UserContextSchema = z.infer<typeof userContextSchema>; export type UserContextSchema = z.infer<typeof userContextSchema>;

79
go.mod
View File

@@ -1,27 +1,19 @@
module tinyauth module tinyauth
go 1.24.0 go 1.23.2
toolchain go1.24.3
require ( require (
github.com/cenkalti/backoff/v5 v5.0.3 github.com/cenkalti/backoff/v5 v5.0.3
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.27.0 github.com/go-playground/validator/v10 v10.27.0
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/google/go-querystring v1.1.0 github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mdp/qrterminal/v3 v3.2.1 github.com/mdp/qrterminal/v3 v3.2.1
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.20.1
github.com/traefik/paerser v0.2.2 github.com/traefik/paerser v0.2.2
github.com/weppos/publicsuffix-go v0.50.0 golang.org/x/crypto v0.41.0
golang.org/x/crypto v0.42.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
gorm.io/gorm v1.31.0
gotest.tools/v3 v3.5.2
) )
require ( require (
@@ -31,34 +23,22 @@ require (
github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/term v0.34.0 // indirect
golang.org/x/term v0.35.0 // indirect gotest.tools/v3 v3.5.2 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.2 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect github.com/boombuler/barcode v1.0.2 // indirect
@@ -74,26 +54,31 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.4.0+incompatible github.com/docker/docker v28.3.3+incompatible
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator v9.31.0+incompatible
github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-json v0.10.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.10
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
@@ -107,28 +92,30 @@ require (
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.13.0 // indirect golang.org/x/arch v0.13.0 // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.31.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

222
go.sum
View File

@@ -4,8 +4,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@@ -74,8 +74,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -88,25 +88,21 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 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-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -115,32 +111,33 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -157,26 +154,25 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -188,8 +184,6 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
@@ -215,22 +209,19 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -240,36 +231,37 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ= github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
@@ -278,60 +270,84 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/weppos/publicsuffix-go v0.50.0 h1:M178k6l8cnh9T1c1cStkhytVxdk5zPd6gGZf8ySIuVo=
github.com/weppos/publicsuffix-go v0.50.0/go.mod h1:VXhClBYMlDrUsome4pOTpe68Ui0p6iQRAbyHQD1yKoU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
@@ -346,36 +362,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@@ -4,12 +4,7 @@ import (
"embed" "embed"
) )
// Frontend // Frontend assets
// //
//go:embed dist //go:embed dist
var FrontendAssets embed.FS var FrontendAssets embed.FS
// Migrations
//
//go:embed migrations/*.sql
var Migrations embed.FS

View File

@@ -1 +0,0 @@
DROP TABLE IF EXISTS "sessions";

View File

@@ -1,10 +0,0 @@
CREATE TABLE IF NOT EXISTS "sessions" (
"uuid" TEXT NOT NULL PRIMARY KEY UNIQUE,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"totp_pending" BOOLEAN NOT NULL,
"oauth_groups" TEXT NULL,
"expiry" INTEGER NOT NULL
);

View File

@@ -1 +0,0 @@
ALTER TABLE "sessions" DROP COLUMN "oauth_name";

View File

@@ -1,10 +0,0 @@
ALTER TABLE "sessions" ADD COLUMN "oauth_name" TEXT;
UPDATE "sessions"
SET "oauth_name" = CASE
WHEN LOWER("provider") = 'github' THEN 'GitHub'
WHEN LOWER("provider") = 'google' THEN 'Google'
ELSE UPPER(SUBSTR("provider", 1, 1)) || SUBSTR("provider", 2)
END
WHERE "oauth_name" IS NULL AND "provider" IS NOT NULL;

View File

@@ -1,14 +1,8 @@
package bootstrap package bootstrap
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net/http"
"net/url"
"os"
"strings" "strings"
"time"
"tinyauth/internal/config" "tinyauth/internal/config"
"tinyauth/internal/controller" "tinyauth/internal/controller"
"tinyauth/internal/middleware" "tinyauth/internal/middleware"
@@ -33,70 +27,74 @@ type Service interface {
} }
type BootstrapApp struct { type BootstrapApp struct {
config config.Config Config config.Config
uuid string
} }
func NewBootstrapApp(config config.Config) *BootstrapApp { func NewBootstrapApp(config config.Config) *BootstrapApp {
return &BootstrapApp{ return &BootstrapApp{
config: config, Config: config,
} }
} }
func (app *BootstrapApp) Setup() error { func (app *BootstrapApp) Setup() error {
// Parse users // Parse users
users, err := utils.GetUsers(app.config.Users, app.config.UsersFile) users, err := utils.GetUsers(app.Config.Users, app.Config.UsersFile)
if err != nil { if err != nil {
return err return err
} }
// Get OAuth configs // Get domain
oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL) domain, err := utils.GetUpperDomain(app.Config.AppURL)
if err != nil {
return err
}
// Get cookie domain
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
if err != nil { if err != nil {
return err return err
} }
// Cookie names // Cookie names
appUrl, _ := url.Parse(app.config.AppURL) // Already validated cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0])
uuid := utils.GenerateUUID(appUrl.Hostname())
app.uuid = uuid
cookieId := strings.Split(uuid, "-")[0]
sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
// Secrets
encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption")
if err != nil {
return err
}
hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac")
if err != nil {
return err
}
// Create configs // Create configs
authConfig := service.AuthServiceConfig{ authConfig := service.AuthServiceConfig{
Users: users, Users: users,
OauthWhitelist: app.config.OAuthWhitelist, OauthWhitelist: app.Config.OAuthWhitelist,
SessionExpiry: app.config.SessionExpiry, SessionExpiry: app.Config.SessionExpiry,
SecureCookie: app.config.SecureCookie, SecureCookie: app.Config.SecureCookie,
CookieDomain: cookieDomain, Domain: domain,
LoginTimeout: app.config.LoginTimeout, LoginTimeout: app.Config.LoginTimeout,
LoginMaxRetries: app.config.LoginMaxRetries, LoginMaxRetries: app.Config.LoginMaxRetries,
SessionCookieName: sessionCookieName, SessionCookieName: sessionCookieName,
HMACSecret: hmacSecret,
EncryptionSecret: encryptionSecret,
} }
// Setup services // Setup services
var ldapService *service.LdapService var ldapService *service.LdapService
if app.config.LdapAddress != "" { if app.Config.LdapAddress != "" {
ldapConfig := service.LdapServiceConfig{ ldapConfig := service.LdapServiceConfig{
Address: app.config.LdapAddress, Address: app.Config.LdapAddress,
BindDN: app.config.LdapBindDN, BindDN: app.Config.LdapBindDN,
BindPassword: app.config.LdapBindPassword, BindPassword: app.Config.LdapBindPassword,
BaseDN: app.config.LdapBaseDN, BaseDN: app.Config.LdapBaseDN,
Insecure: app.config.LdapInsecure, Insecure: app.Config.LdapInsecure,
SearchFilter: app.config.LdapSearchFilter, SearchFilter: app.Config.LdapSearchFilter,
} }
ldapService = service.NewLdapService(ldapConfig) ldapService = service.NewLdapService(ldapConfig)
@@ -109,25 +107,9 @@ func (app *BootstrapApp) Setup() error {
} }
} }
// Bootstrap database
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
DatabasePath: app.config.DatabasePath,
})
log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service")
err = databaseService.Init()
if err != nil {
return fmt.Errorf("failed to initialize database service: %w", err)
}
database := databaseService.GetDatabase()
// Create services
dockerService := service.NewDockerService() dockerService := service.NewDockerService()
authService := service.NewAuthService(authConfig, dockerService, ldapService, database) authService := service.NewAuthService(authConfig, dockerService, ldapService)
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders) oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig())
// Initialize services // Initialize services
services := []Service{ services := []Service{
@@ -147,62 +129,30 @@ func (app *BootstrapApp) Setup() error {
} }
// Configured providers // Configured providers
configuredProviders := make([]controller.Provider, 0) var configuredProviders []string
for id, provider := range oauthProviders {
if id == "" {
continue
}
if provider.Name == "" {
if name, ok := config.OverrideProviders[id]; ok {
provider.Name = name
} else {
provider.Name = utils.Capitalize(id)
}
}
configuredProviders = append(configuredProviders, controller.Provider{
Name: provider.Name,
ID: id,
OAuth: true,
})
}
if authService.UserAuthConfigured() || ldapService != nil { if authService.UserAuthConfigured() || ldapService != nil {
configuredProviders = append(configuredProviders, controller.Provider{ configuredProviders = append(configuredProviders, "username")
Name: "Username",
ID: "username",
OAuth: false,
})
} }
log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers") configuredProviders = append(configuredProviders, oauthBrokerService.GetConfiguredServices()...)
if len(configuredProviders) == 0 { if len(configuredProviders) == 0 {
return fmt.Errorf("no authentication providers configured") return fmt.Errorf("no authentication providers configured")
} }
// Create engine // Create engine
if config.Version != "development" {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.New() engine := gin.New()
if len(app.config.TrustedProxies) > 0 { if config.Version != "development" {
err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) gin.SetMode(gin.ReleaseMode)
if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err)
}
} }
// Create middlewares // Create middlewares
var middlewares []Middleware var middlewares []Middleware
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
CookieDomain: cookieDomain, Domain: domain,
}, authService, oauthBrokerService) }, authService, oauthBrokerService)
uiMiddleware := middleware.NewUIMiddleware() uiMiddleware := middleware.NewUIMiddleware()
@@ -225,34 +175,34 @@ func (app *BootstrapApp) Setup() error {
// Create controllers // Create controllers
contextController := controller.NewContextController(controller.ContextControllerConfig{ contextController := controller.NewContextController(controller.ContextControllerConfig{
Providers: configuredProviders, ConfiguredProviders: configuredProviders,
Title: app.config.Title, DisableContinue: app.Config.DisableContinue,
AppURL: app.config.AppURL, Title: app.Config.Title,
CookieDomain: cookieDomain, GenericName: app.Config.GenericName,
ForgotPasswordMessage: app.config.ForgotPasswordMessage, Domain: domain,
BackgroundImage: app.config.BackgroundImage, ForgotPasswordMessage: app.Config.FogotPasswordMessage,
OAuthAutoRedirect: app.config.OAuthAutoRedirect, BackgroundImage: app.Config.BackgroundImage,
OAuthAutoRedirect: app.Config.OAuthAutoRedirect,
}, apiRouter) }, apiRouter)
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{ oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
AppURL: app.config.AppURL, AppURL: app.Config.AppURL,
SecureCookie: app.config.SecureCookie, SecureCookie: app.Config.SecureCookie,
CSRFCookieName: csrfCookieName, CSRFCookieName: csrfCookieName,
RedirectCookieName: redirectCookieName, RedirectCookieName: redirectCookieName,
CookieDomain: cookieDomain, Domain: domain,
}, apiRouter, authService, oauthBrokerService) }, apiRouter, authService, oauthBrokerService)
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL, AppURL: app.Config.AppURL,
}, apiRouter, dockerService, authService) }, apiRouter, dockerService, authService)
userController := controller.NewUserController(controller.UserControllerConfig{ userController := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: cookieDomain, Domain: domain,
}, apiRouter, authService) }, apiRouter, authService)
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: app.config.ResourcesDir, ResourcesDir: app.Config.ResourcesDir,
ResourcesDisabled: app.config.DisableResources,
}, mainRouter) }, mainRouter)
healthController := controller.NewHealthController(apiRouter) healthController := controller.NewHealthController(apiRouter)
@@ -272,14 +222,8 @@ func (app *BootstrapApp) Setup() error {
ctrl.SetupRoutes() ctrl.SetupRoutes()
} }
// If analytics are not disabled, start heartbeat
if !app.config.DisableAnalytics {
log.Debug().Msg("Starting heartbeat routine")
go app.heartbeat()
}
// Start server // Start server
address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port) address := fmt.Sprintf("%s:%d", app.Config.Address, app.Config.Port)
log.Info().Msgf("Starting server on %s", address) log.Info().Msgf("Starting server on %s", address)
if err := engine.Run(address); err != nil { if err := engine.Run(address); err != nil {
log.Fatal().Err(err).Msg("Failed to start server") log.Fatal().Err(err).Msg("Failed to start server")
@@ -288,54 +232,29 @@ func (app *BootstrapApp) Setup() error {
return nil return nil
} }
func (app *BootstrapApp) heartbeat() { // Temporary
ticker := time.NewTicker(time.Duration(12) * time.Hour) func (app *BootstrapApp) getOAuthBrokerConfig() map[string]config.OAuthServiceConfig {
defer ticker.Stop() return map[string]config.OAuthServiceConfig{
"google": {
type heartbeat struct { ClientID: app.Config.GoogleClientId,
UUID string `json:"uuid"` ClientSecret: app.Config.GoogleClientSecret,
Version string `json:"version"` RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", app.Config.AppURL),
},
"github": {
ClientID: app.Config.GithubClientId,
ClientSecret: app.Config.GithubClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", app.Config.AppURL),
},
"generic": {
ClientID: app.Config.GenericClientId,
ClientSecret: app.Config.GenericClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", app.Config.AppURL),
Scopes: strings.Split(app.Config.GenericScopes, ","),
AuthURL: app.Config.GenericAuthURL,
TokenURL: app.Config.GenericTokenURL,
UserinfoURL: app.Config.GenericUserURL,
InsecureSkipVerify: app.Config.GenericSkipSSL,
},
} }
var body heartbeat
body.UUID = app.uuid
body.Version = config.Version
bodyJson, err := json.Marshal(body)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal heartbeat body")
return
}
client := &http.Client{}
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
for ; true; <-ticker.C {
log.Debug().Msg("Sending heartbeat")
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
if err != nil {
log.Error().Err(err).Msg("Failed to create heartbeat request")
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Failed to send heartbeat")
continue
}
res.Body.Close()
if res.StatusCode != 200 && res.StatusCode != 201 {
log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
}
}
} }

View File

@@ -1,50 +1,5 @@
package config package config
// Version information, set at build time
var Version = "development"
var CommitHash = "development"
var BuildTimestamp = "0000-00-00T00:00:00Z"
// Cookie name templates
var SessionCookieName = "tinyauth-session"
var CSRFCookieName = "tinyauth-csrf"
var RedirectCookieName = "tinyauth-redirect"
// Main app config
type Config struct {
Port int `mapstructure:"port" validate:"required"`
Address string `validate:"required,ip4_addr" mapstructure:"address"`
AppURL string `validate:"required,url" mapstructure:"app-url"`
Users string `mapstructure:"users"`
UsersFile string `mapstructure:"users-file"`
SecureCookie bool `mapstructure:"secure-cookie"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect"`
SessionExpiry int `mapstructure:"session-expiry"`
LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"`
Title string `mapstructure:"app-title"`
LoginTimeout int `mapstructure:"login-timeout"`
LoginMaxRetries int `mapstructure:"login-max-retries"`
ForgotPasswordMessage string `mapstructure:"forgot-password-message"`
BackgroundImage string `mapstructure:"background-image" validate:"required"`
LdapAddress string `mapstructure:"ldap-address"`
LdapBindDN string `mapstructure:"ldap-bind-dn"`
LdapBindPassword string `mapstructure:"ldap-bind-password"`
LdapBaseDN string `mapstructure:"ldap-base-dn"`
LdapInsecure bool `mapstructure:"ldap-insecure"`
LdapSearchFilter string `mapstructure:"ldap-search-filter"`
ResourcesDir string `mapstructure:"resources-dir"`
DatabasePath string `mapstructure:"database-path" validate:"required"`
TrustedProxies string `mapstructure:"trusted-proxies"`
DisableAnalytics bool `mapstructure:"disable-analytics"`
DisableResources bool `mapstructure:"disable-resources"`
}
// OAuth/OIDC config
type Claims struct { type Claims struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
@@ -52,26 +7,99 @@ type Claims struct {
Groups any `json:"groups"` Groups any `json:"groups"`
} }
var Version = "development"
var CommitHash = "n/a"
var BuildTimestamp = "n/a"
var SessionCookieName = "tinyauth-session"
var CSRFCookieName = "tinyauth-csrf"
var RedirectCookieName = "tinyauth-redirect"
type Config struct {
Port int `mapstructure:"port" validate:"required"`
Address string `validate:"required,ip4_addr" mapstructure:"address"`
Secret string `validate:"required,len=32" mapstructure:"secret"`
SecretFile string `mapstructure:"secret-file"`
AppURL string `validate:"required,url" mapstructure:"app-url"`
Users string `mapstructure:"users"`
UsersFile string `mapstructure:"users-file"`
SecureCookie bool `mapstructure:"secure-cookie"`
GithubClientId string `mapstructure:"github-client-id"`
GithubClientSecret string `mapstructure:"github-client-secret"`
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
GoogleClientId string `mapstructure:"google-client-id"`
GoogleClientSecret string `mapstructure:"google-client-secret"`
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
GenericClientId string `mapstructure:"generic-client-id"`
GenericClientSecret string `mapstructure:"generic-client-secret"`
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
GenericScopes string `mapstructure:"generic-scopes"`
GenericAuthURL string `mapstructure:"generic-auth-url"`
GenericTokenURL string `mapstructure:"generic-token-url"`
GenericUserURL string `mapstructure:"generic-user-url"`
GenericName string `mapstructure:"generic-name"`
GenericSkipSSL bool `mapstructure:"generic-skip-ssl"`
DisableContinue bool `mapstructure:"disable-continue"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"`
SessionExpiry int `mapstructure:"session-expiry"`
LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"`
Title string `mapstructure:"app-title"`
LoginTimeout int `mapstructure:"login-timeout"`
LoginMaxRetries int `mapstructure:"login-max-retries"`
FogotPasswordMessage string `mapstructure:"forgot-password-message"`
BackgroundImage string `mapstructure:"background-image" validate:"required"`
LdapAddress string `mapstructure:"ldap-address"`
LdapBindDN string `mapstructure:"ldap-bind-dn"`
LdapBindPassword string `mapstructure:"ldap-bind-password"`
LdapBaseDN string `mapstructure:"ldap-base-dn"`
LdapInsecure bool `mapstructure:"ldap-insecure"`
LdapSearchFilter string `mapstructure:"ldap-search-filter"`
ResourcesDir string `mapstructure:"resources-dir"`
}
type OAuthLabels struct {
Whitelist string
Groups string
}
type BasicLabels struct {
Username string
Password PasswordLabels
}
type PasswordLabels struct {
Plain string
File string
}
type IPLabels struct {
Allow []string
Block []string
Bypass []string
}
type Labels struct {
Users string
Allowed string
Headers []string
Domain []string
Basic BasicLabels
OAuth OAuthLabels
IP IPLabels
}
type OAuthServiceConfig struct { type OAuthServiceConfig struct {
ClientID string `key:"client-id"` ClientID string
ClientSecret string `key:"client-secret"` ClientSecret string
ClientSecretFile string `key:"client-secret-file"` Scopes []string
Scopes []string `key:"scopes"` RedirectURL string
RedirectURL string `key:"redirect-url"` AuthURL string
AuthURL string `key:"auth-url"` TokenURL string
TokenURL string `key:"token-url"` UserinfoURL string
UserinfoURL string `key:"user-info-url"` InsecureSkipVerify bool
InsecureSkipVerify bool `key:"insecure-skip-verify"`
Name string `key:"name"`
} }
var OverrideProviders = map[string]string{
"google": "Google",
"github": "GitHub",
}
// User/session related stuff
type User struct { type User struct {
Username string Username string
Password string Password string
@@ -84,14 +112,12 @@ type UserSearch struct {
} }
type SessionCookie struct { type SessionCookie struct {
UUID string
Username string Username string
Name string Name string
Email string Email string
Provider string Provider string
TotpPending bool TotpPending bool
OAuthGroups string OAuthGroups string
OAuthName string
} }
type UserContext struct { type UserContext struct {
@@ -104,11 +130,8 @@ type UserContext struct {
TotpPending bool TotpPending bool
OAuthGroups string OAuthGroups string
TotpEnabled bool TotpEnabled bool
OAuthName string
} }
// API responses and queries
type UnauthorizedQuery struct { type UnauthorizedQuery struct {
Username string `url:"username"` Username string `url:"username"`
Resource string `url:"resource"` Resource string `url:"resource"`
@@ -119,64 +142,3 @@ type UnauthorizedQuery struct {
type RedirectQuery struct { type RedirectQuery struct {
RedirectURI string `url:"redirect_uri"` RedirectURI string `url:"redirect_uri"`
} }
// Labels
type Apps struct {
Apps map[string]App
}
type App struct {
Config AppConfig
Users AppUsers
OAuth AppOAuth
IP AppIP
Response AppResponse
Path AppPath
}
type AppConfig struct {
Domain string
}
type AppUsers struct {
Allow string
Block string
}
type AppOAuth struct {
Whitelist string
Groups string
}
type AppIP struct {
Allow []string
Block []string
Bypass []string
}
type AppResponse struct {
Headers []string
BasicAuth AppBasicAuth
}
type AppBasicAuth struct {
Username string
Password string
PasswordFile string
}
type AppPath struct {
Allow string
Block string
}
// Flags
type Providers struct {
Providers map[string]OAuthServiceConfig
}
// API server
var ApiServer = "https://api.tinyauth.app"

View File

@@ -1,8 +1,6 @@
package controller package controller
import ( import (
"fmt"
"net/url"
"tinyauth/internal/utils" "tinyauth/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -17,53 +15,48 @@ type UserContextResponse struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Provider string `json:"provider"` Provider string `json:"provider"`
OAuth bool `json:"oauth"` Oauth bool `json:"oauth"`
TotpPending bool `json:"totpPending"` TotpPending bool `json:"totpPending"`
OAuthName string `json:"oauthName"`
} }
type AppContextResponse struct { type AppContextResponse struct {
Status int `json:"status"` Status int `json:"status"`
Message string `json:"message"` Message string `json:"message"`
Providers []Provider `json:"providers"` ConfiguredProviders []string `json:"configuredProviders"`
Title string `json:"title"` DisableContinue bool `json:"disableContinue"`
AppURL string `json:"appUrl"` Title string `json:"title"`
CookieDomain string `json:"cookieDomain"` GenericName string `json:"genericName"`
ForgotPasswordMessage string `json:"forgotPasswordMessage"` Domain string `json:"domain"`
BackgroundImage string `json:"backgroundImage"` ForgotPasswordMessage string `json:"forgotPasswordMessage"`
OAuthAutoRedirect string `json:"oauthAutoRedirect"` BackgroundImage string `json:"backgroundImage"`
} OAuthAutoRedirect string `json:"oauthAutoRedirect"`
type Provider struct {
Name string `json:"name"`
ID string `json:"id"`
OAuth bool `json:"oauth"`
} }
type ContextControllerConfig struct { type ContextControllerConfig struct {
Providers []Provider ConfiguredProviders []string
DisableContinue bool
Title string Title string
AppURL string GenericName string
CookieDomain string Domain string
ForgotPasswordMessage string ForgotPasswordMessage string
BackgroundImage string BackgroundImage string
OAuthAutoRedirect string OAuthAutoRedirect string
} }
type ContextController struct { type ContextController struct {
config ContextControllerConfig Config ContextControllerConfig
router *gin.RouterGroup Router *gin.RouterGroup
} }
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController { func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
return &ContextController{ return &ContextController{
config: config, Config: config,
router: router, Router: router,
} }
} }
func (controller *ContextController) SetupRoutes() { func (controller *ContextController) SetupRoutes() {
contextGroup := controller.router.Group("/context") contextGroup := controller.Router.Group("/context")
contextGroup.GET("/user", controller.userContextHandler) contextGroup.GET("/user", controller.userContextHandler)
contextGroup.GET("/app", controller.appContextHandler) contextGroup.GET("/app", controller.appContextHandler)
} }
@@ -79,9 +72,8 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
Name: context.Name, Name: context.Name,
Email: context.Email, Email: context.Email,
Provider: context.Provider, Provider: context.Provider,
OAuth: context.OAuth, Oauth: context.OAuth,
TotpPending: context.TotpPending, TotpPending: context.TotpPending,
OAuthName: context.OAuthName,
} }
if err != nil { if err != nil {
@@ -97,17 +89,16 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
} }
func (controller *ContextController) appContextHandler(c *gin.Context) { func (controller *ContextController) appContextHandler(c *gin.Context) {
appUrl, _ := url.Parse(controller.config.AppURL) // no need to check error, validated on startup
c.JSON(200, AppContextResponse{ c.JSON(200, AppContextResponse{
Status: 200, Status: 200,
Message: "Success", Message: "Success",
Providers: controller.config.Providers, ConfiguredProviders: controller.Config.ConfiguredProviders,
Title: controller.config.Title, DisableContinue: controller.Config.DisableContinue,
AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), Title: controller.Config.Title,
CookieDomain: controller.config.CookieDomain, GenericName: controller.Config.GenericName,
ForgotPasswordMessage: controller.config.ForgotPasswordMessage, Domain: controller.Config.Domain,
BackgroundImage: controller.config.BackgroundImage, ForgotPasswordMessage: controller.Config.ForgotPasswordMessage,
OAuthAutoRedirect: controller.config.OAuthAutoRedirect, BackgroundImage: controller.Config.BackgroundImage,
OAuthAutoRedirect: controller.Config.OAuthAutoRedirect,
}) })
} }

View File

@@ -1,144 +0,0 @@
package controller_test
import (
"encoding/json"
"net/http/httptest"
"testing"
"tinyauth/internal/config"
"tinyauth/internal/controller"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
)
var controllerCfg = controller.ContextControllerConfig{
Providers: []controller.Provider{
{
Name: "Username",
ID: "username",
OAuth: false,
},
{
Name: "Google",
ID: "google",
OAuth: true,
},
},
Title: "Test App",
AppURL: "http://localhost:8080",
CookieDomain: "localhost",
ForgotPasswordMessage: "Contact admin to reset your password.",
BackgroundImage: "/assets/bg.jpg",
OAuthAutoRedirect: "google",
}
var userContext = config.UserContext{
Username: "testuser",
Name: "testuser",
Email: "test@example.com",
IsLoggedIn: true,
OAuth: false,
Provider: "username",
TotpPending: false,
OAuthGroups: "",
TotpEnabled: false,
}
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()
recorder := httptest.NewRecorder()
if middlewares != nil {
for _, m := range *middlewares {
router.Use(m)
}
}
group := router.Group("/api")
ctrl := controller.NewContextController(controllerCfg, group)
ctrl.SetupRoutes()
return router, recorder
}
func TestAppContextHandler(t *testing.T) {
expectedRes := controller.AppContextResponse{
Status: 200,
Message: "Success",
Providers: controllerCfg.Providers,
Title: controllerCfg.Title,
AppURL: controllerCfg.AppURL,
CookieDomain: controllerCfg.CookieDomain,
ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage,
BackgroundImage: controllerCfg.BackgroundImage,
OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect,
}
router, recorder := setupContextController(nil)
req := httptest.NewRequest("GET", "/api/context/app", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
var ctrlRes controller.AppContextResponse
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
assert.NilError(t, err)
assert.DeepEqual(t, expectedRes, ctrlRes)
}
func TestUserContextHandler(t *testing.T) {
expectedRes := controller.UserContextResponse{
Status: 200,
Message: "Success",
IsLoggedIn: userContext.IsLoggedIn,
Username: userContext.Username,
Name: userContext.Name,
Email: userContext.Email,
Provider: userContext.Provider,
OAuth: userContext.OAuth,
TotpPending: userContext.TotpPending,
}
// Test with context
router, recorder := setupContextController(&[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &userContext)
c.Next()
},
})
req := httptest.NewRequest("GET", "/api/context/user", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
var ctrlRes controller.UserContextResponse
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
assert.NilError(t, err)
assert.DeepEqual(t, expectedRes, ctrlRes)
// Test no context
expectedRes = controller.UserContextResponse{
Status: 401,
Message: "Unauthorized",
IsLoggedIn: false,
}
router, recorder = setupContextController(nil)
req = httptest.NewRequest("GET", "/api/context/user", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
assert.NilError(t, err)
assert.DeepEqual(t, expectedRes, ctrlRes)
}

View File

@@ -3,18 +3,18 @@ package controller
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
type HealthController struct { type HealthController struct {
router *gin.RouterGroup Router *gin.RouterGroup
} }
func NewHealthController(router *gin.RouterGroup) *HealthController { func NewHealthController(router *gin.RouterGroup) *HealthController {
return &HealthController{ return &HealthController{
router: router, Router: router,
} }
} }
func (controller *HealthController) SetupRoutes() { func (controller *HealthController) SetupRoutes() {
controller.router.GET("/health", controller.healthHandler) controller.Router.GET("/health", controller.healthHandler)
controller.router.HEAD("/health", controller.healthHandler) controller.Router.HEAD("/health", controller.healthHandler)
} }
func (controller *HealthController) healthHandler(c *gin.Context) { func (controller *HealthController) healthHandler(c *gin.Context) {

View File

@@ -23,27 +23,27 @@ type OAuthControllerConfig struct {
RedirectCookieName string RedirectCookieName string
SecureCookie bool SecureCookie bool
AppURL string AppURL string
CookieDomain string Domain string
} }
type OAuthController struct { type OAuthController struct {
config OAuthControllerConfig Config OAuthControllerConfig
router *gin.RouterGroup Router *gin.RouterGroup
auth *service.AuthService Auth *service.AuthService
broker *service.OAuthBrokerService Broker *service.OAuthBrokerService
} }
func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController { func NewOAuthController(config OAuthControllerConfig, router *gin.RouterGroup, auth *service.AuthService, broker *service.OAuthBrokerService) *OAuthController {
return &OAuthController{ return &OAuthController{
config: config, Config: config,
router: router, Router: router,
auth: auth, Auth: auth,
broker: broker, Broker: broker,
} }
} }
func (controller *OAuthController) SetupRoutes() { func (controller *OAuthController) SetupRoutes() {
oauthGroup := controller.router.Group("/oauth") oauthGroup := controller.Router.Group("/oauth")
oauthGroup.GET("/url/:provider", controller.oauthURLHandler) oauthGroup.GET("/url/:provider", controller.oauthURLHandler)
oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler) oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler)
} }
@@ -61,7 +61,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return return
} }
service, exists := controller.broker.GetService(req.Provider) service, exists := controller.Broker.GetService(req.Provider)
if !exists { if !exists {
log.Warn().Msgf("OAuth provider not found: %s", req.Provider) log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
@@ -74,13 +74,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
state := service.GenerateState() state := service.GenerateState()
authURL := service.GetAuthURL(state) authURL := service.GetAuthURL(state)
c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true)
redirectURI := c.Query("redirect_uri") redirectURI := c.Query("redirect_uri")
if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) { if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.Domain) {
log.Debug().Msg("Setting redirect URI cookie") log.Debug().Msg("Setting redirect URI cookie")
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@@ -104,59 +104,58 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
} }
state := c.Query("state") state := c.Query("state")
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName) csrfCookie, err := c.Cookie(controller.Config.CSRFCookieName)
if err != nil || state != csrfCookie { if err != nil || state != csrfCookie {
log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return return
} }
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true)
code := c.Query("code") code := c.Query("code")
service, exists := controller.broker.GetService(req.Provider) service, exists := controller.Broker.GetService(req.Provider)
if !exists { if !exists {
log.Warn().Msgf("OAuth provider not found: %s", req.Provider) log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
err = service.VerifyCode(code) err = service.VerifyCode(code)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to verify OAuth code") log.Error().Err(err).Msg("Failed to verify OAuth code")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
user, err := controller.broker.GetUser(req.Provider) user, err := controller.Broker.GetUser(req.Provider)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get user from OAuth provider") log.Error().Err(err).Msg("Failed to get user from OAuth provider")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
if user.Email == "" { if user.Email == "" {
log.Error().Msg("OAuth provider did not return an email") log.Error().Msg("OAuth provider did not return an email")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
if !controller.auth.IsEmailWhitelisted(user.Email) { if !controller.Auth.EmailWhitelisted(user.Email) {
queries, err := query.Values(config.UnauthorizedQuery{ queries, err := query.Values(config.UnauthorizedQuery{
Username: user.Email, Username: user.Email,
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query") log.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode()))
return return
} }
@@ -170,36 +169,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
} }
var username string controller.Auth.CreateSessionCookie(c, &config.SessionCookie{
Username: user.Email,
if user.PreferredUsername != "" {
log.Debug().Msg("Using preferred username from OAuth provider")
username = user.PreferredUsername
} else {
log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
username = strings.Replace(user.Email, "@", "_", -1)
}
err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{
Username: username,
Name: name, Name: name,
Email: user.Email, Email: user.Email,
Provider: req.Provider, Provider: req.Provider,
OAuthGroups: utils.CoalesceToString(user.Groups), OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: service.GetName(),
}) })
if err != nil { redirectURI, err := c.Cookie(controller.Config.RedirectCookieName)
log.Error().Err(err).Msg("Failed to create session cookie")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
redirectURI, err := c.Cookie(controller.config.RedirectCookieName) if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.Domain) {
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
log.Debug().Msg("No redirect URI cookie found, redirecting to app root") log.Debug().Msg("No redirect URI cookie found, redirecting to app root")
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL) c.Redirect(http.StatusTemporaryRedirect, controller.Config.AppURL)
return return
} }
@@ -209,10 +191,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to encode redirect URI query") log.Error().Err(err).Msg("Failed to encode redirect URI query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode())) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.Config.AppURL, queries.Encode()))
} }

View File

@@ -22,23 +22,23 @@ type ProxyControllerConfig struct {
} }
type ProxyController struct { type ProxyController struct {
config ProxyControllerConfig Config ProxyControllerConfig
router *gin.RouterGroup Router *gin.RouterGroup
docker *service.DockerService Docker *service.DockerService
auth *service.AuthService Auth *service.AuthService
} }
func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController { func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController {
return &ProxyController{ return &ProxyController{
config: config, Config: config,
router: router, Router: router,
docker: docker, Docker: docker,
auth: auth, Auth: auth,
} }
} }
func (controller *ProxyController) SetupRoutes() { func (controller *ProxyController) SetupRoutes() {
proxyGroup := controller.router.Group("/auth") proxyGroup := controller.Router.Group("/auth")
proxyGroup.GET("/:proxy", controller.proxyHandler) proxyGroup.GET("/:proxy", controller.proxyHandler)
} }
@@ -55,15 +55,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return return
} }
if req.Proxy != "nginx" && req.Proxy != "traefik" && req.Proxy != "caddy" {
log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return
}
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
if isBrowser { if isBrowser {
@@ -76,18 +67,43 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
proto := c.Request.Header.Get("X-Forwarded-Proto") proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host") host := c.Request.Header.Get("X-Forwarded-Host")
labels, err := controller.docker.GetLabels(host) hostWithoutPort := strings.Split(host, ":")[0]
id := strings.Split(hostWithoutPort, ".")[0]
labels, err := controller.Docker.GetLabels(id, hostWithoutPort)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get labels from Docker") log.Error().Err(err).Msg("Failed to get labels from Docker")
controller.handleError(c, req, isBrowser)
if req.Proxy == "nginx" || !isBrowser {
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
clientIP := c.ClientIP() clientIP := c.ClientIP()
if controller.auth.IsBypassedIP(labels.IP, clientIP) { if controller.Auth.BypassedIP(labels, clientIP) {
controller.setHeaders(c, labels) c.Header("Authorization", c.Request.Header.Get("Authorization"))
headers := utils.ParseHeaders(labels.Headers)
for key, value := range headers {
log.Debug().Str("header", key).Msg("Setting header")
c.Header(key, value)
}
if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" {
log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File))))
}
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"message": "Authenticated", "message": "Authenticated",
@@ -95,25 +111,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return return
} }
authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) if !controller.Auth.CheckIP(labels, clientIP) {
if err != nil {
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
controller.handleError(c, req, isBrowser)
return
}
if !authEnabled {
log.Debug().Msg("Authentication disabled for resource, allowing access")
controller.setHeaders(c, labels)
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
})
return
}
if !controller.auth.CheckIP(labels.IP, clientIP) {
if req.Proxy == "nginx" || !isBrowser { if req.Proxy == "nginx" || !isBrowser {
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
@@ -129,11 +127,52 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query") log.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode()))
return
}
authEnabled, err := controller.Auth.AuthEnabled(uri, labels)
if err != nil {
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
if req.Proxy == "nginx" || !isBrowser {
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return
}
if !authEnabled {
log.Debug().Msg("Authentication disabled for resource, allowing access")
c.Header("Authorization", c.Request.Header.Get("Authorization"))
headers := utils.ParseHeaders(labels.Headers)
for key, value := range headers {
log.Debug().Str("header", key).Msg("Setting header")
c.Header(key, value)
}
if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" {
log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File))))
}
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
})
return return
} }
@@ -156,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
} }
if userContext.IsLoggedIn { if userContext.IsLoggedIn {
appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) appAllowed := controller.Auth.ResourceAllowed(c, userContext, labels)
if !appAllowed { if !appAllowed {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
@@ -173,24 +212,24 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
Resource: strings.Split(host, ".")[0], Resource: strings.Split(host, ".")[0],
}) })
if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
if userContext.OAuth { if userContext.OAuth {
queries.Set("username", userContext.Email) queries.Set("username", userContext.Email)
} else { } else {
queries.Set("username", userContext.Username) queries.Set("username", userContext.Username)
} }
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode()))
return return
} }
if userContext.OAuth { if userContext.OAuth {
groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) groupOK := controller.Auth.OAuthGroup(c, userContext, labels)
if !groupOK { if !groupOK {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
@@ -208,29 +247,40 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
GroupErr: true, GroupErr: true,
}) })
if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
if userContext.OAuth { if userContext.OAuth {
queries.Set("username", userContext.Email) queries.Set("username", userContext.Email)
} else { } else {
queries.Set("username", userContext.Username) queries.Set("username", userContext.Username)
} }
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())) if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.Config.AppURL, queries.Encode()))
return return
} }
} }
c.Header("Authorization", c.Request.Header.Get("Authorization"))
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)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
controller.setHeaders(c, labels) headers := utils.ParseHeaders(labels.Headers)
for key, value := range headers {
log.Debug().Str("header", key).Msg("Setting header")
c.Header(key, value)
}
if labels.Basic.Username != "" && utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File) != "" {
log.Debug().Str("username", labels.Basic.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Basic.Username, utils.GetSecret(labels.Basic.Password.Plain, labels.Basic.Password.File))))
}
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -253,39 +303,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to encode redirect URI query") log.Error().Err(err).Msg("Failed to encode redirect URI query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.Config.AppURL))
return return
} }
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.Config.AppURL, queries.Encode()))
}
func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) {
c.Header("Authorization", c.Request.Header.Get("Authorization"))
headers := utils.ParseHeaders(labels.Response.Headers)
for key, value := range headers {
log.Debug().Str("header", key).Msg("Setting header")
c.Header(key, value)
}
basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile)
if labels.Response.BasicAuth.Username != "" && basicPassword != "" {
log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword)))
}
}
func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrowser bool) {
if req.Proxy == "nginx" || !isBrowser {
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
} }

View File

@@ -1,164 +0,0 @@
package controller_test
import (
"net/http/httptest"
"testing"
"tinyauth/internal/config"
"tinyauth/internal/controller"
"tinyauth/internal/service"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
)
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()
if middlewares != nil {
for _, m := range *middlewares {
router.Use(m)
}
}
group := router.Group("/api")
recorder := httptest.NewRecorder()
// Database
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
DatabasePath: "/tmp/tinyauth_test.db",
})
assert.NilError(t, databaseService.Init())
database := databaseService.GetDatabase()
// Docker
dockerService := service.NewDockerService()
assert.NilError(t, dockerService.Init())
// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{
{
Username: "testuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
},
},
OauthWhitelist: "",
SessionExpiry: 3600,
SecureCookie: false,
CookieDomain: "localhost",
LoginTimeout: 300,
LoginMaxRetries: 3,
SessionCookieName: "tinyauth-session",
}, dockerService, nil, database)
// Controller
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: "http://localhost:8080",
}, group, dockerService, authService)
ctrl.SetupRoutes()
return router, recorder, authService
}
func TestProxyHandler(t *testing.T) {
// Setup
router, recorder, authService := setupProxyController(t, nil)
// Test invalid proxy
req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 400, recorder.Code)
// Test logged out user (traefik/caddy)
recorder = httptest.NewRecorder()
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "example.com")
req.Header.Set("X-Forwarded-Uri", "/somepath")
req.Header.Set("Accept", "text/html")
router.ServeHTTP(recorder, req)
assert.Equal(t, 307, recorder.Code)
assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location"))
// Test logged out user (nginx)
recorder = httptest.NewRecorder()
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
// Test logged in user
c := gin.CreateTestContextOnly(recorder, router)
err := authService.CreateSessionCookie(c, &config.SessionCookie{
Username: "testuser",
Name: "testuser",
Email: "testuser@example.com",
Provider: "username",
TotpPending: false,
OAuthGroups: "",
})
assert.NilError(t, err)
cookie := c.Writer.Header().Get("Set-Cookie")
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "testuser",
Name: "testuser",
Email: "testuser@example.com",
IsLoggedIn: true,
OAuth: false,
Provider: "username",
TotpPending: false,
OAuthGroups: "",
TotpEnabled: false,
})
c.Next()
},
})
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("Cookie", cookie)
req.Header.Set("Accept", "text/html")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "testuser", recorder.Header().Get("Remote-User"))
assert.Equal(t, "testuser", recorder.Header().Get("Remote-Name"))
assert.Equal(t, "testuser@example.com", recorder.Header().Get("Remote-Email"))
// Ensure basic auth is disabled for TOTP enabled users
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "testuser",
Name: "testuser",
Email: "testuser@example.com",
IsLoggedIn: true,
OAuth: false,
Provider: "basic",
TotpPending: false,
OAuthGroups: "",
TotpEnabled: true,
})
c.Next()
},
})
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.SetBasicAuth("testuser", "test")
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
}

View File

@@ -7,44 +7,36 @@ import (
) )
type ResourcesControllerConfig struct { type ResourcesControllerConfig struct {
ResourcesDir string ResourcesDir string
ResourcesDisabled bool
} }
type ResourcesController struct { type ResourcesController struct {
config ResourcesControllerConfig Config ResourcesControllerConfig
router *gin.RouterGroup Router *gin.RouterGroup
fileServer http.Handler FileServer http.Handler
} }
func NewResourcesController(config ResourcesControllerConfig, router *gin.RouterGroup) *ResourcesController { func NewResourcesController(config ResourcesControllerConfig, router *gin.RouterGroup) *ResourcesController {
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.ResourcesDir))) fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.ResourcesDir)))
return &ResourcesController{ return &ResourcesController{
config: config, Config: config,
router: router, Router: router,
fileServer: fileServer, FileServer: fileServer,
} }
} }
func (controller *ResourcesController) SetupRoutes() { func (controller *ResourcesController) SetupRoutes() {
controller.router.GET("/resources/*resource", controller.resourcesHandler) controller.Router.GET("/resources/*resource", controller.resourcesHandler)
} }
func (controller *ResourcesController) resourcesHandler(c *gin.Context) { func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
if controller.config.ResourcesDir == "" { if controller.Config.ResourcesDir == "" {
c.JSON(404, gin.H{ c.JSON(404, gin.H{
"status": 404, "status": 404,
"message": "Resources not found", "message": "Resources not found",
}) })
return return
} }
if controller.config.ResourcesDisabled { controller.FileServer.ServeHTTP(c.Writer, c.Request)
c.JSON(403, gin.H{
"status": 403,
"message": "Resources are disabled",
})
return
}
controller.fileServer.ServeHTTP(c.Writer, c.Request)
} }

View File

@@ -1,57 +0,0 @@
package controller_test
import (
"net/http/httptest"
"os"
"testing"
"tinyauth/internal/controller"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
)
func TestResourcesHandler(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
router := gin.New()
group := router.Group("/")
ctrl := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: "/tmp/tinyauth",
}, group)
ctrl.SetupRoutes()
// Create test data
err := os.Mkdir("/tmp/tinyauth", 0755)
assert.NilError(t, err)
defer os.RemoveAll("/tmp/tinyauth")
file, err := os.Create("/tmp/tinyauth/test.txt")
assert.NilError(t, err)
_, err = file.WriteString("This is a test file.")
assert.NilError(t, err)
file.Close()
// Test existing file
req := httptest.NewRequest("GET", "/resources/test.txt", nil)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "This is a test file.", recorder.Body.String())
// Test non-existing file
req = httptest.NewRequest("GET", "/resources/nonexistent.txt", nil)
recorder = httptest.NewRecorder()
router.ServeHTTP(recorder, req)
assert.Equal(t, 404, recorder.Code)
// Test directory traversal attack
req = httptest.NewRequest("GET", "/resources/../etc/passwd", nil)
recorder = httptest.NewRecorder()
router.ServeHTTP(recorder, req)
assert.Equal(t, 404, recorder.Code)
}

View File

@@ -22,25 +22,25 @@ type TotpRequest struct {
} }
type UserControllerConfig struct { type UserControllerConfig struct {
CookieDomain string Domain string
} }
type UserController struct { type UserController struct {
config UserControllerConfig Config UserControllerConfig
router *gin.RouterGroup Router *gin.RouterGroup
auth *service.AuthService Auth *service.AuthService
} }
func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController { func NewUserController(config UserControllerConfig, router *gin.RouterGroup, auth *service.AuthService) *UserController {
return &UserController{ return &UserController{
config: config, Config: config,
router: router, Router: router,
auth: auth, Auth: auth,
} }
} }
func (controller *UserController) SetupRoutes() { func (controller *UserController) SetupRoutes() {
userGroup := controller.router.Group("/user") userGroup := controller.Router.Group("/user")
userGroup.POST("/login", controller.loginHandler) userGroup.POST("/login", controller.loginHandler)
userGroup.POST("/logout", controller.logoutHandler) userGroup.POST("/logout", controller.logoutHandler)
userGroup.POST("/totp", controller.totpHandler) userGroup.POST("/totp", controller.totpHandler)
@@ -69,7 +69,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt") log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt")
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier) isLocked, remainingTime := controller.Auth.IsAccountLocked(rateIdentifier)
if isLocked { if isLocked {
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts") log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts")
@@ -80,11 +80,11 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return return
} }
userSearch := controller.auth.SearchUser(req.Username) userSearch := controller.Auth.SearchUser(req.Username)
if userSearch.Type == "unknown" { if userSearch.Type == "" {
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found") log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found")
controller.auth.RecordLoginAttempt(rateIdentifier, false) controller.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -92,9 +92,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return return
} }
if !controller.auth.VerifyUser(userSearch, req.Password) { if !controller.Auth.VerifyUser(userSearch, req.Password) {
log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password") log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password")
controller.auth.RecordLoginAttempt(rateIdentifier, false) controller.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -104,18 +104,18 @@ func (controller *UserController) loginHandler(c *gin.Context) {
log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful") log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful")
controller.auth.RecordLoginAttempt(rateIdentifier, true) controller.Auth.RecordLoginAttempt(rateIdentifier, true)
if userSearch.Type == "local" { if userSearch.Type == "local" {
user := controller.auth.GetLocalUser(userSearch.Username) user := controller.Auth.GetLocalUser(userSearch.Username)
if user.TotpSecret != "" { if user.TotpSecret != "" {
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification") log.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, &config.SessionCookie{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(req.Username), Name: utils.Capitalize(req.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain),
Provider: "username", Provider: "username",
TotpPending: true, TotpPending: true,
}) })
@@ -138,10 +138,10 @@ func (controller *UserController) loginHandler(c *gin.Context) {
} }
} }
err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{
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: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain),
Provider: "username", Provider: "username",
}) })
@@ -163,7 +163,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
func (controller *UserController) logoutHandler(c *gin.Context) { func (controller *UserController) logoutHandler(c *gin.Context) {
log.Debug().Msg("Logout request received") log.Debug().Msg("Logout request received")
controller.auth.DeleteSessionCookie(c) controller.Auth.DeleteSessionCookie(c)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -214,24 +214,24 @@ func (controller *UserController) totpHandler(c *gin.Context) {
log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt") log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt")
isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier) isLocked, remainingTime := controller.Auth.IsAccountLocked(rateIdentifier)
if isLocked { if isLocked {
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts") log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts")
c.JSON(429, gin.H{ c.JSON(429, gin.H{
"status": 429, "status": 429,
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remainingTime), "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
}) })
return return
} }
user := controller.auth.GetLocalUser(context.Username) user := controller.Auth.GetLocalUser(context.Username)
ok := totp.Validate(req.Code, user.TotpSecret) ok := totp.Validate(req.Code, user.TotpSecret)
if !ok { if !ok {
log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code") log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code")
controller.auth.RecordLoginAttempt(rateIdentifier, false) controller.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -241,12 +241,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful") log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful")
controller.auth.RecordLoginAttempt(rateIdentifier, true) controller.Auth.RecordLoginAttempt(rateIdentifier, true)
err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{
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: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.Domain),
Provider: "username", Provider: "username",
}) })

View File

@@ -1,297 +0,0 @@
package controller_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"tinyauth/internal/config"
"tinyauth/internal/controller"
"tinyauth/internal/service"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
"gotest.tools/v3/assert"
)
var cookieValue string
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()
if middlewares != nil {
for _, m := range *middlewares {
router.Use(m)
}
}
group := router.Group("/api")
recorder := httptest.NewRecorder()
// Database
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
DatabasePath: "/tmp/tinyauth_test.db",
})
assert.NilError(t, databaseService.Init())
database := databaseService.GetDatabase()
// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{
{
Username: "testuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
},
{
Username: "totpuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
TotpSecret: totpSecret,
},
},
OauthWhitelist: "",
SessionExpiry: 3600,
SecureCookie: false,
CookieDomain: "localhost",
LoginTimeout: 300,
LoginMaxRetries: 3,
SessionCookieName: "tinyauth-session",
}, nil, nil, database)
// Controller
ctrl := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: "localhost",
}, group, authService)
ctrl.SetupRoutes()
return router, recorder
}
func TestLoginHandler(t *testing.T) {
// Setup
router, recorder := setupUserController(t, nil)
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "test",
}
loginReqJson, err := json.Marshal(loginReq)
assert.NilError(t, err)
// Test
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.Assert(t, cookie.Value != "")
cookieValue = cookie.Value
// Test invalid credentials
loginReq = controller.LoginRequest{
Username: "testuser",
Password: "invalid",
}
loginReqJson, err = json.Marshal(loginReq)
assert.NilError(t, err)
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
// Test totp required
loginReq = controller.LoginRequest{
Username: "totpuser",
Password: "test",
}
loginReqJson, err = json.Marshal(loginReq)
assert.NilError(t, err)
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
loginResJson, err := json.Marshal(map[string]any{
"message": "TOTP required",
"status": 200,
"totpPending": true,
})
assert.NilError(t, err)
assert.Equal(t, string(loginResJson), recorder.Body.String())
// Test invalid json
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader("{invalid json}"))
router.ServeHTTP(recorder, req)
assert.Equal(t, 400, recorder.Code)
// Test rate limiting
loginReq = controller.LoginRequest{
Username: "testuser",
Password: "invalid",
}
loginReqJson, err = json.Marshal(loginReq)
assert.NilError(t, err)
for range 5 {
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
}
assert.Equal(t, 429, recorder.Code)
}
func TestLogoutHandler(t *testing.T) {
// Setup
router, recorder := setupUserController(t, nil)
// Test
req := httptest.NewRequest("POST", "/api/user/logout", nil)
req.AddCookie(&http.Cookie{
Name: "tinyauth-session",
Value: cookieValue,
})
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.Equal(t, "", cookie.Value)
assert.Equal(t, -1, cookie.MaxAge)
}
func TestTotpHandler(t *testing.T) {
// Setup
router, recorder := setupUserController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "totpuser",
Name: "totpuser",
Email: "totpuser@example.com",
IsLoggedIn: false,
OAuth: false,
Provider: "username",
TotpPending: true,
OAuthGroups: "",
TotpEnabled: true,
})
c.Next()
},
})
// Test
code, err := totp.GenerateCode(totpSecret, time.Now())
assert.NilError(t, err)
totpReq := controller.TotpRequest{
Code: code,
}
totpReqJson, err := json.Marshal(totpReq)
assert.NilError(t, err)
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.Assert(t, cookie.Value != "")
// Test invalid json
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader("{invalid json}"))
router.ServeHTTP(recorder, req)
assert.Equal(t, 400, recorder.Code)
// Test rate limiting
totpReq = controller.TotpRequest{
Code: "000000",
}
totpReqJson, err = json.Marshal(totpReq)
assert.NilError(t, err)
for range 5 {
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
}
assert.Equal(t, 429, recorder.Code)
// Test invalid code
router, recorder = setupUserController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "totpuser",
Name: "totpuser",
Email: "totpuser@example.com",
IsLoggedIn: false,
OAuth: false,
Provider: "username",
TotpPending: true,
OAuthGroups: "",
TotpEnabled: true,
})
c.Next()
},
})
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
// Test no totp pending
router, recorder = setupUserController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "totpuser",
Name: "totpuser",
Email: "totpuser@example.com",
IsLoggedIn: false,
OAuth: false,
Provider: "username",
TotpPending: false,
OAuthGroups: "",
TotpEnabled: false,
})
c.Next()
},
})
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
}

View File

@@ -12,20 +12,20 @@ import (
) )
type ContextMiddlewareConfig struct { type ContextMiddlewareConfig struct {
CookieDomain string Domain string
} }
type ContextMiddleware struct { type ContextMiddleware struct {
config ContextMiddlewareConfig Config ContextMiddlewareConfig
auth *service.AuthService Auth *service.AuthService
broker *service.OAuthBrokerService Broker *service.OAuthBrokerService
} }
func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService) *ContextMiddleware { func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService) *ContextMiddleware {
return &ContextMiddleware{ return &ContextMiddleware{
config: config, Config: config,
auth: auth, Auth: auth,
broker: broker, Broker: broker,
} }
} }
@@ -35,7 +35,7 @@ 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) {
cookie, err := m.auth.GetSessionCookie(c) cookie, err := m.Auth.GetSessionCookie(c)
if err != nil { if err != nil {
log.Debug().Err(err).Msg("No valid session cookie found") log.Debug().Err(err).Msg("No valid session cookie found")
@@ -57,11 +57,11 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
switch cookie.Provider { switch cookie.Provider {
case "username": case "username":
userSearch := m.auth.SearchUser(cookie.Username) userSearch := m.Auth.SearchUser(cookie.Username)
if userSearch.Type == "unknown" || userSearch.Type == "error" { if userSearch.Type == "unknown" {
log.Debug().Msg("User from session cookie not found") log.Debug().Msg("User from session cookie not found")
m.auth.DeleteSessionCookie(c) m.Auth.DeleteSessionCookie(c)
goto basic goto basic
} }
@@ -75,17 +75,17 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
c.Next() c.Next()
return return
default: default:
_, exists := m.broker.GetService(cookie.Provider) _, exists := m.Broker.GetService(cookie.Provider)
if !exists { if !exists {
log.Debug().Msg("OAuth provider from session cookie not found") log.Debug().Msg("OAuth provider from session cookie not found")
m.auth.DeleteSessionCookie(c) m.Auth.DeleteSessionCookie(c)
goto basic goto basic
} }
if !m.auth.IsEmailWhitelisted(cookie.Email) { if !m.Auth.EmailWhitelisted(cookie.Email) {
log.Debug().Msg("Email from session cookie not whitelisted") log.Debug().Msg("Email from session cookie not whitelisted")
m.auth.DeleteSessionCookie(c) m.Auth.DeleteSessionCookie(c)
goto basic goto basic
} }
@@ -95,7 +95,6 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
Email: cookie.Email, Email: cookie.Email,
Provider: cookie.Provider, Provider: cookie.Provider,
OAuthGroups: cookie.OAuthGroups, OAuthGroups: cookie.OAuthGroups,
OAuthName: cookie.OAuthName,
IsLoggedIn: true, IsLoggedIn: true,
OAuth: true, OAuth: true,
}) })
@@ -104,7 +103,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
} }
basic: basic:
basic := m.auth.GetBasicAuth(c) basic := m.Auth.GetBasicAuth(c)
if basic == nil { if basic == nil {
log.Debug().Msg("No basic auth provided") log.Debug().Msg("No basic auth provided")
@@ -112,15 +111,15 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
return return
} }
userSearch := m.auth.SearchUser(basic.Username) userSearch := m.Auth.SearchUser(basic.Username)
if userSearch.Type == "unknown" || userSearch.Type == "error" { if userSearch.Type == "unknown" {
log.Debug().Msg("User from basic auth not found") log.Debug().Msg("User from basic auth not found")
c.Next() c.Next()
return return
} }
if !m.auth.VerifyUser(userSearch, basic.Password) { if !m.Auth.VerifyUser(userSearch, basic.Password) {
log.Debug().Msg("Invalid password for basic auth user") log.Debug().Msg("Invalid password for basic auth user")
c.Next() c.Next()
return return
@@ -130,12 +129,12 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
case "local": case "local":
log.Debug().Msg("Basic auth user is local") log.Debug().Msg("Basic auth user is local")
user := m.auth.GetLocalUser(basic.Username) user := m.Auth.GetLocalUser(basic.Username)
c.Set("context", &config.UserContext{ c.Set("context", &config.UserContext{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(user.Username), Name: utils.Capitalize(user.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.Domain),
Provider: "basic", Provider: "basic",
IsLoggedIn: true, IsLoggedIn: true,
TotpEnabled: user.TotpSecret != "", TotpEnabled: user.TotpSecret != "",
@@ -147,7 +146,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
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: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.Domain),
Provider: "basic", Provider: "basic",
IsLoggedIn: true, IsLoggedIn: true,
}) })

View File

@@ -11,8 +11,8 @@ import (
) )
type UIMiddleware struct { type UIMiddleware struct {
uiFs fs.FS UIFS fs.FS
uiFileServer http.Handler UIFileServer http.Handler
} }
func NewUIMiddleware() *UIMiddleware { func NewUIMiddleware() *UIMiddleware {
@@ -26,8 +26,8 @@ func (m *UIMiddleware) Init() error {
return err return err
} }
m.uiFs = ui m.UIFS = ui
m.uiFileServer = http.FileServer(http.FS(ui)) m.UIFileServer = http.FileServer(http.FS(ui))
return nil return nil
} }
@@ -42,13 +42,13 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
c.Next() c.Next()
return return
default: default:
_, err := fs.Stat(m.uiFs, strings.TrimPrefix(c.Request.URL.Path, "/")) _, err := fs.Stat(m.UIFS, strings.TrimPrefix(c.Request.URL.Path, "/"))
if os.IsNotExist(err) { if os.IsNotExist(err) {
c.Request.URL.Path = "/" c.Request.URL.Path = "/"
} }
m.uiFileServer.ServeHTTP(c.Writer, c.Request) m.UIFileServer.ServeHTTP(c.Writer, c.Request)
c.Abort() c.Abort()
return return
} }

View File

@@ -49,24 +49,18 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
latency := time.Since(tStart).String() latency := time.Since(tStart).String()
subLogger := log.With().Str("method", method). // logPath check if the path should be logged normally or with debug
Str("path", path).
Str("address", address).
Str("client_ip", clientIP).
Int("status", code).
Str("latency", latency).Logger()
if m.logPath(method + " " + path) { if m.logPath(method + " " + path) {
switch { switch {
case code >= 400 && code < 500: case code >= 200 && code < 300:
subLogger.Warn().Msg("Client Error") log.Info().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
case code >= 500: case code >= 300 && code < 400:
subLogger.Error().Msg("Server Error") log.Warn().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
default: case code >= 400:
subLogger.Info().Msg("Request") log.Error().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
} }
} else { } else {
subLogger.Debug().Msg("Request") log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request")
} }
} }
} }

View File

@@ -1,13 +0,0 @@
package model
type Session struct {
UUID string `gorm:"column:uuid;primaryKey"`
Username string `gorm:"column:username"`
Email string `gorm:"column:email"`
Name string `gorm:"column:name"`
Provider string `gorm:"column:provider"`
TOTPPending bool `gorm:"column:totp_pending"`
OAuthGroups string `gorm:"column:oauth_groups"`
Expiry int64 `gorm:"column:expiry"`
OAuthName string `gorm:"column:oauth_name"`
}

View File

@@ -7,14 +7,12 @@ import (
"sync" "sync"
"time" "time"
"tinyauth/internal/config" "tinyauth/internal/config"
"tinyauth/internal/model"
"tinyauth/internal/utils" "tinyauth/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/gorilla/sessions"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
) )
type LoginAttempt struct { type LoginAttempt struct {
@@ -28,35 +26,62 @@ type AuthServiceConfig struct {
OauthWhitelist string OauthWhitelist string
SessionExpiry int SessionExpiry int
SecureCookie bool SecureCookie bool
CookieDomain string Domain string
LoginTimeout int LoginTimeout int
LoginMaxRetries int LoginMaxRetries int
SessionCookieName string SessionCookieName string
HMACSecret string
EncryptionSecret string
} }
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 LoginMutex sync.RWMutex
ldap *LdapService Store *sessions.CookieStore
database *gorm.DB LDAP *LdapService
} }
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService { func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService) *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, LDAP: ldap,
database: database,
} }
} }
func (auth *AuthService) Init() error { func (auth *AuthService) Init() error {
store := sessions.NewCookieStore([]byte(auth.Config.HMACSecret), []byte(auth.Config.EncryptionSecret))
store.Options = &sessions.Options{
Path: "/",
MaxAge: auth.Config.SessionExpiry,
Secure: auth.Config.SecureCookie,
HttpOnly: true,
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
}
auth.Store = store
return nil return nil
} }
func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) {
session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName)
// If there was an error getting the session, it might be invalid so let's clear it and retry
if err != nil {
log.Debug().Err(err).Msg("Error getting session, creating a new one")
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
session, err = auth.Store.New(c.Request, auth.Config.SessionCookieName)
if err != nil {
return nil, err
}
}
return session, nil
}
func (auth *AuthService) SearchUser(username string) config.UserSearch { func (auth *AuthService) SearchUser(username string) config.UserSearch {
if auth.GetLocalUser(username).Username != "" { if auth.GetLocalUser(username).Username != "" {
return config.UserSearch{ return config.UserSearch{
@@ -65,14 +90,12 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
} }
} }
if auth.ldap != nil { if auth.LDAP != nil {
userDN, err := auth.ldap.Search(username) userDN, err := auth.LDAP.Search(username)
if err != nil { if err != nil {
log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP") log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
return config.UserSearch{ return config.UserSearch{}
Type: "error",
}
} }
return config.UserSearch{ return config.UserSearch{
@@ -92,14 +115,14 @@ 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 != nil {
err := auth.ldap.Bind(search.Username, password) err := auth.LDAP.Bind(search.Username, password)
if err != nil { if err != nil {
log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP") log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
return false return false
} }
err = auth.ldap.Bind(auth.ldap.Config.BindDN, auth.ldap.Config.BindPassword) err = auth.LDAP.Bind(auth.LDAP.Config.BindDN, auth.LDAP.Config.BindPassword)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication") log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
return false return false
@@ -117,7 +140,7 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
} }
func (auth *AuthService) GetLocalUser(username string) config.User { func (auth *AuthService) GetLocalUser(username string) config.User {
for _, user := range auth.config.Users { for _, user := range auth.Config.Users {
if user.Username == username { if user.Username == username {
return user return user
} }
@@ -132,227 +155,219 @@ func (auth *AuthService) CheckPassword(user config.User, password string) bool {
} }
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
auth.loginMutex.RLock() auth.LoginMutex.RLock()
defer auth.loginMutex.RUnlock() defer auth.LoginMutex.RUnlock()
if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 { // Return false if rate limiting is not configured
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return false, 0 return false, 0
} }
attempt, exists := auth.loginAttempts[identifier] // Check if the identifier exists in the map
attempt, exists := auth.LoginAttempts[identifier]
if !exists { if !exists {
return false, 0 return false, 0
} }
// If account is locked, check if lock time has expired
if attempt.LockedUntil.After(time.Now()) { if attempt.LockedUntil.After(time.Now()) {
// Calculate remaining lockout time in seconds
remaining := int(time.Until(attempt.LockedUntil).Seconds()) remaining := int(time.Until(attempt.LockedUntil).Seconds())
return true, remaining return true, remaining
} }
// Lock has expired
return false, 0 return false, 0
} }
func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 { // Skip if rate limiting is not configured
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return return
} }
auth.loginMutex.Lock() auth.LoginMutex.Lock()
defer auth.loginMutex.Unlock() defer auth.LoginMutex.Unlock()
attempt, exists := auth.loginAttempts[identifier] // Get current attempt record or create a new one
attempt, exists := auth.LoginAttempts[identifier]
if !exists { if !exists {
attempt = &LoginAttempt{} attempt = &LoginAttempt{}
auth.loginAttempts[identifier] = attempt auth.LoginAttempts[identifier] = attempt
} }
// Update last attempt time
attempt.LastAttempt = time.Now() attempt.LastAttempt = time.Now()
// If successful login, reset failed attempts
if success { if success {
attempt.FailedAttempts = 0 attempt.FailedAttempts = 0
attempt.LockedUntil = time.Time{} // Reset lock time attempt.LockedUntil = time.Time{} // Reset lock time
return return
} }
// Increment failed attempts
attempt.FailedAttempts++ attempt.FailedAttempts++
if attempt.FailedAttempts >= auth.config.LoginMaxRetries { // If max retries reached, lock the account
attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second) if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
log.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts") attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
} }
} }
func (auth *AuthService) IsEmailWhitelisted(email string) bool { func (auth *AuthService) EmailWhitelisted(email string) bool {
return utils.CheckFilter(auth.config.OauthWhitelist, email) return utils.CheckFilter(auth.Config.OauthWhitelist, email)
} }
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
uuid, err := uuid.NewRandom() session, err := auth.GetSession(c)
if err != nil { if err != nil {
return err return err
} }
var expiry int var sessionExpiry int
if data.TotpPending { if data.TotpPending {
expiry = 3600 sessionExpiry = 3600
} else { } else {
expiry = auth.config.SessionExpiry sessionExpiry = auth.Config.SessionExpiry
} }
session := model.Session{ session.Values["username"] = data.Username
UUID: uuid.String(), session.Values["name"] = data.Name
Username: data.Username, session.Values["email"] = data.Email
Email: data.Email, session.Values["provider"] = data.Provider
Name: data.Name, session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
Provider: data.Provider, session.Values["totpPending"] = data.TotpPending
TOTPPending: data.TotpPending, session.Values["oauthGroups"] = data.OAuthGroups
OAuthGroups: data.OAuthGroups,
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
OAuthName: data.OAuthName,
}
err = auth.database.Create(&session).Error
err = session.Save(c.Request, c.Writer)
if err != nil { if err != nil {
return err return err
} }
c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
return nil return nil
} }
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
cookie, err := c.Cookie(auth.config.SessionCookieName) session, err := auth.GetSession(c)
if err != nil { if err != nil {
return err return err
} }
res := auth.database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{}) // Delete all values in the session
for key := range session.Values {
if res.Error != nil { delete(session.Values, key)
return res.Error
} }
c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) err = session.Save(c.Request, c.Writer)
if err != nil {
return err
}
// Clear the cookie in the browser
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
return nil return nil
} }
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) { func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) {
cookie, err := c.Cookie(auth.config.SessionCookieName) session, err := auth.GetSession(c)
if err != nil { if err != nil {
return config.SessionCookie{}, err return config.SessionCookie{}, err
} }
var session model.Session username, usernameOk := session.Values["username"].(string)
email, emailOk := session.Values["email"].(string)
name, nameOk := session.Values["name"].(string)
provider, providerOK := session.Values["provider"].(string)
expiry, expiryOk := session.Values["expiry"].(int64)
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
res := auth.database.Unscoped().Where("uuid = ?", cookie).First(&session) // If any data is missing, delete the session cookie
if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
if res.Error != nil { log.Warn().Msg("Session cookie is invalid")
return config.SessionCookie{}, res.Error auth.DeleteSessionCookie(c)
return config.SessionCookie{}, nil
} }
if res.RowsAffected == 0 { // If the session cookie has expired, delete it
return config.SessionCookie{}, fmt.Errorf("session not found") if time.Now().Unix() > expiry {
} log.Warn().Msg("Session cookie expired")
auth.DeleteSessionCookie(c)
currentTime := time.Now().Unix() return config.SessionCookie{}, nil
if currentTime > session.Expiry {
res := auth.database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{})
if res.Error != nil {
log.Error().Err(res.Error).Msg("Failed to delete expired session")
}
return config.SessionCookie{}, fmt.Errorf("session expired")
} }
return config.SessionCookie{ return config.SessionCookie{
UUID: session.UUID, Username: username,
Username: session.Username, Name: name,
Email: session.Email, Email: email,
Name: session.Name, Provider: provider,
Provider: session.Provider, TotpPending: totpPending,
TotpPending: session.TOTPPending, OAuthGroups: oauthGroups,
OAuthGroups: session.OAuthGroups,
OAuthName: session.OAuthName,
}, nil }, nil
} }
func (auth *AuthService) UserAuthConfigured() bool { func (auth *AuthService) UserAuthConfigured() bool {
return len(auth.config.Users) > 0 || auth.ldap != nil // If there are users or LDAP is configured, return true
return len(auth.Config.Users) > 0 || auth.LDAP != nil
} }
func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool {
if context.OAuth { if context.OAuth {
log.Debug().Msg("Checking OAuth whitelist") log.Debug().Msg("Checking OAuth whitelist")
return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) return utils.CheckFilter(labels.OAuth.Whitelist, context.Email)
} }
if labels.Users.Block != "" {
log.Debug().Msg("Checking blocked users")
if utils.CheckFilter(labels.Users.Block, context.Username) {
return false
}
}
log.Debug().Msg("Checking users") log.Debug().Msg("Checking users")
return utils.CheckFilter(labels.Users.Allow, context.Username) return utils.CheckFilter(labels.Users, context.Username)
} }
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool { func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool {
if requiredGroups == "" { if labels.OAuth.Groups == "" {
return true return true
} }
for id := range config.OverrideProviders { if context.Provider != "generic" {
if context.Provider == id { log.Debug().Msg("Not using generic provider, skipping group check")
log.Info().Str("provider", id).Msg("OAuth groups not supported for this provider") return true
return true }
}
} // Split the groups by comma (no need to parse since they are from the API response)
oauthGroups := strings.Split(context.OAuthGroups, ",")
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) { // For every group check if it is in the required groups
for _, group := range oauthGroups {
if utils.CheckFilter(labels.OAuth.Groups, group) {
return true return true
} }
} }
// No groups matched
log.Debug().Msg("No groups matched") log.Debug().Msg("No groups matched")
return false return false
} }
func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, error) {
// Check for block list // If the label is empty, auth is enabled
if path.Block != "" { if labels.Allowed == "" {
regex, err := regexp.Compile(path.Block) return true, nil
if err != nil {
return true, err
}
if !regex.MatchString(uri) {
return false, nil
}
} }
// Check for allow list regex, err := regexp.Compile(labels.Allowed)
if path.Allow != "" {
regex, err := regexp.Compile(path.Allow)
if err != nil { if err != nil {
return true, err return true, err
}
if regex.MatchString(uri) {
return false, nil
}
} }
// If the regex matches the URI, auth is not enabled
if regex.MatchString(uri) {
return false, nil
}
// Auth enabled
return true, nil return true, nil
} }
@@ -368,8 +383,9 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
} }
} }
func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
for _, blocked := range labels.Block { // Check if the IP is in block list
for _, blocked := range labels.IP.Block {
res, err := utils.FilterIP(blocked, ip) res, err := utils.FilterIP(blocked, ip)
if err != nil { if err != nil {
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
@@ -381,7 +397,8 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
} }
} }
for _, allowed := range labels.Allow { // For every IP in the allow list, check if the IP matches
for _, allowed := range labels.IP.Allow {
res, err := utils.FilterIP(allowed, ip) res, err := utils.FilterIP(allowed, ip)
if err != nil { if err != nil {
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
@@ -393,7 +410,8 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
} }
} }
if len(labels.Allow) > 0 { // If not in allowed range and allowed range is not empty, deny access
if len(labels.IP.Allow) > 0 {
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
return false return false
} }
@@ -402,8 +420,9 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
return true return true
} }
func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { func (auth *AuthService) BypassedIP(labels config.Labels, ip string) bool {
for _, bypassed := range labels.Bypass { // For every IP in the bypass list, check if the IP matches
for _, bypassed := range labels.IP.Bypass {
res, err := utils.FilterIP(bypassed, ip) res, err := utils.FilterIP(bypassed, ip)
if err != nil { if err != nil {
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list") log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")

View File

@@ -1,78 +0,0 @@
package service
import (
"database/sql"
"tinyauth/internal/assets"
"github.com/glebarez/sqlite"
"github.com/golang-migrate/migrate/v4"
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
"gorm.io/gorm"
)
type DatabaseServiceConfig struct {
DatabasePath string
}
type DatabaseService struct {
config DatabaseServiceConfig
database *gorm.DB
}
func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService {
return &DatabaseService{
config: config,
}
}
func (ds *DatabaseService) Init() error {
gormDB, err := gorm.Open(sqlite.Open(ds.config.DatabasePath), &gorm.Config{})
if err != nil {
return err
}
sqlDB, err := gormDB.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(1)
err = ds.migrateDatabase(sqlDB)
if err != nil && err != migrate.ErrNoChange {
return err
}
ds.database = gormDB
return nil
}
func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error {
data, err := iofs.New(assets.Migrations, "migrations")
if err != nil {
return err
}
target, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{})
if err != nil {
return err
}
migrator, err := migrate.NewWithInstance("iofs", data, "tinyauth", target)
if err != nil {
return err
}
return migrator.Up()
}
func (ds *DatabaseService) GetDatabase() *gorm.DB {
return ds.database
}

View File

@@ -4,7 +4,9 @@ import (
"context" "context"
"strings" "strings"
"tinyauth/internal/config" "tinyauth/internal/config"
"tinyauth/internal/utils/decoders" "tinyauth/internal/utils"
"slices"
container "github.com/docker/docker/api/types/container" container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@@ -12,8 +14,8 @@ import (
) )
type DockerService struct { type DockerService struct {
client *client.Client Client *client.Client
context context.Context Context context.Context
} }
func NewDockerService() *DockerService { func NewDockerService() *DockerService {
@@ -29,13 +31,13 @@ func (docker *DockerService) Init() error {
ctx := context.Background() ctx := context.Background()
client.NegotiateAPIVersion(ctx) client.NegotiateAPIVersion(ctx)
docker.client = client docker.Client = client
docker.context = ctx docker.Context = ctx
return nil return nil
} }
func (docker *DockerService) GetContainers() ([]container.Summary, error) { func (docker *DockerService) GetContainers() ([]container.Summary, error) {
containers, err := docker.client.ContainerList(docker.context, container.ListOptions{}) containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -43,7 +45,7 @@ func (docker *DockerService) GetContainers() ([]container.Summary, error) {
} }
func (docker *DockerService) InspectContainer(containerId string) (container.InspectResponse, error) { func (docker *DockerService) InspectContainer(containerId string) (container.InspectResponse, error) {
inspect, err := docker.client.ContainerInspect(docker.context, containerId) inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
if err != nil { if err != nil {
return container.InspectResponse{}, err return container.InspectResponse{}, err
} }
@@ -51,49 +53,48 @@ func (docker *DockerService) InspectContainer(containerId string) (container.Ins
} }
func (docker *DockerService) DockerConnected() bool { func (docker *DockerService) DockerConnected() bool {
_, err := docker.client.Ping(docker.context) _, err := docker.Client.Ping(docker.Context)
return err == nil return err == nil
} }
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { func (docker *DockerService) GetLabels(app string, domain string) (config.Labels, error) {
isConnected := docker.DockerConnected() isConnected := docker.DockerConnected()
if !isConnected { if !isConnected {
log.Debug().Msg("Docker not connected, returning empty labels") log.Debug().Msg("Docker not connected, returning empty labels")
return config.App{}, nil return config.Labels{}, nil
} }
containers, err := docker.GetContainers() containers, err := docker.GetContainers()
if err != nil { if err != nil {
return config.App{}, err return config.Labels{}, err
} }
for _, ctr := range containers { for _, container := range containers {
inspect, err := docker.InspectContainer(ctr.ID) inspect, err := docker.InspectContainer(container.ID)
if err != nil { if err != nil {
log.Warn().Str("id", ctr.ID).Err(err).Msg("Error inspecting container, skipping") log.Warn().Str("id", container.ID).Err(err).Msg("Error inspecting container, skipping")
continue continue
} }
labels, err := decoders.DecodeLabels(inspect.Config.Labels) labels, err := utils.GetLabels(inspect.Config.Labels)
if err != nil { if err != nil {
log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") log.Warn().Str("id", container.ID).Err(err).Msg("Error getting container labels, skipping")
continue continue
} }
for appName, appLabels := range labels.Apps { // Check if the container matches the ID or domain
if appLabels.Config.Domain == appDomain { if slices.Contains(labels.Domain, domain) {
log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain") log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain")
return appLabels, nil return labels, nil
} }
if strings.TrimPrefix(inspect.Name, "/") == appName { if strings.TrimPrefix(inspect.Name, "/") == app {
log.Debug().Str("id", inspect.ID).Msg("Found matching container by app name") log.Debug().Str("id", inspect.ID).Msg("Found matching container by name")
return appLabels, nil return labels, nil
}
} }
} }
log.Debug().Msg("No matching container found, returning empty labels") log.Debug().Msg("No matching container found, returning empty labels")
return config.App{}, nil return config.Labels{}, nil
} }

View File

@@ -16,18 +16,17 @@ import (
) )
type GenericOAuthService struct { type GenericOAuthService struct {
config oauth2.Config Config oauth2.Config
context context.Context Context context.Context
token *oauth2.Token Token *oauth2.Token
verifier string Verifier string
insecureSkipVerify bool InsecureSkipVerify bool
userinfoUrl string UserinfoURL string
name string
} }
func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService { func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService {
return &GenericOAuthService{ return &GenericOAuthService{
config: oauth2.Config{ Config: oauth2.Config{
ClientID: config.ClientID, ClientID: config.ClientID,
ClientSecret: config.ClientSecret, ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL, RedirectURL: config.RedirectURL,
@@ -37,16 +36,15 @@ func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthServi
TokenURL: config.TokenURL, TokenURL: config.TokenURL,
}, },
}, },
insecureSkipVerify: config.InsecureSkipVerify, InsecureSkipVerify: config.InsecureSkipVerify,
userinfoUrl: config.UserinfoURL, UserinfoURL: config.UserinfoURL,
name: config.Name,
} }
} }
func (generic *GenericOAuthService) Init() error { func (generic *GenericOAuthService) Init() error {
transport := &http.Transport{ transport := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: generic.insecureSkipVerify, InsecureSkipVerify: generic.InsecureSkipVerify,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
}, },
} }
@@ -60,8 +58,8 @@ func (generic *GenericOAuthService) Init() error {
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
verifier := oauth2.GenerateVerifier() verifier := oauth2.GenerateVerifier()
generic.context = ctx generic.Context = ctx
generic.verifier = verifier generic.Verifier = verifier
return nil return nil
} }
@@ -76,26 +74,26 @@ func (generic *GenericOAuthService) GenerateState() string {
} }
func (generic *GenericOAuthService) GetAuthURL(state string) string { func (generic *GenericOAuthService) GetAuthURL(state string) string {
return generic.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.verifier)) return generic.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.Verifier))
} }
func (generic *GenericOAuthService) VerifyCode(code string) error { func (generic *GenericOAuthService) VerifyCode(code string) error {
token, err := generic.config.Exchange(generic.context, code, oauth2.VerifierOption(generic.verifier)) token, err := generic.Config.Exchange(generic.Context, code, oauth2.VerifierOption(generic.Verifier))
if err != nil { if err != nil {
return err return err
} }
generic.token = token generic.Token = token
return nil return nil
} }
func (generic *GenericOAuthService) Userinfo() (config.Claims, error) { func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
var user config.Claims var user config.Claims
client := generic.config.Client(generic.context, generic.token) client := generic.Config.Client(generic.Context, generic.Token)
res, err := client.Get(generic.userinfoUrl) res, err := client.Get(generic.UserinfoURL)
if err != nil { if err != nil {
return user, err return user, err
} }
@@ -117,7 +115,3 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
return user, nil return user, nil
} }
func (generic *GenericOAuthService) GetName() string {
return generic.name
}

View File

@@ -29,23 +29,21 @@ type GithubUserInfoResponse struct {
} }
type GithubOAuthService struct { type GithubOAuthService struct {
config oauth2.Config Config oauth2.Config
context context.Context Context context.Context
token *oauth2.Token Token *oauth2.Token
verifier string Verifier string
name string
} }
func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService { func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService {
return &GithubOAuthService{ return &GithubOAuthService{
config: oauth2.Config{ Config: oauth2.Config{
ClientID: config.ClientID, ClientID: config.ClientID,
ClientSecret: config.ClientSecret, ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL, RedirectURL: config.RedirectURL,
Scopes: GithubOAuthScopes, Scopes: GithubOAuthScopes,
Endpoint: endpoints.GitHub, Endpoint: endpoints.GitHub,
}, },
name: config.Name,
} }
} }
@@ -55,8 +53,8 @@ func (github *GithubOAuthService) Init() error {
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
verifier := oauth2.GenerateVerifier() verifier := oauth2.GenerateVerifier()
github.context = ctx github.Context = ctx
github.verifier = verifier github.Verifier = verifier
return nil return nil
} }
@@ -71,24 +69,24 @@ func (github *GithubOAuthService) GenerateState() string {
} }
func (github *GithubOAuthService) GetAuthURL(state string) string { func (github *GithubOAuthService) GetAuthURL(state string) string {
return github.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.verifier)) return github.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.Verifier))
} }
func (github *GithubOAuthService) VerifyCode(code string) error { func (github *GithubOAuthService) VerifyCode(code string) error {
token, err := github.config.Exchange(github.context, code, oauth2.VerifierOption(github.verifier)) token, err := github.Config.Exchange(github.Context, code, oauth2.VerifierOption(github.Verifier))
if err != nil { if err != nil {
return err return err
} }
github.token = token github.Token = token
return nil return nil
} }
func (github *GithubOAuthService) Userinfo() (config.Claims, error) { func (github *GithubOAuthService) Userinfo() (config.Claims, error) {
var user config.Claims var user config.Claims
client := github.config.Client(github.context, github.token) client := github.Config.Client(github.Context, github.Token)
req, err := http.NewRequest("GET", "https://api.github.com/user", nil) req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
if err != nil { if err != nil {
@@ -169,7 +167,3 @@ func (github *GithubOAuthService) Userinfo() (config.Claims, error) {
return user, nil return user, nil
} }
func (github *GithubOAuthService) GetName() string {
return github.name
}

View File

@@ -24,23 +24,21 @@ type GoogleUserInfoResponse struct {
} }
type GoogleOAuthService struct { type GoogleOAuthService struct {
config oauth2.Config Config oauth2.Config
context context.Context Context context.Context
token *oauth2.Token Token *oauth2.Token
verifier string Verifier string
name string
} }
func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService { func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService {
return &GoogleOAuthService{ return &GoogleOAuthService{
config: oauth2.Config{ Config: oauth2.Config{
ClientID: config.ClientID, ClientID: config.ClientID,
ClientSecret: config.ClientSecret, ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL, RedirectURL: config.RedirectURL,
Scopes: GoogleOAuthScopes, Scopes: GoogleOAuthScopes,
Endpoint: endpoints.Google, Endpoint: endpoints.Google,
}, },
name: config.Name,
} }
} }
@@ -50,8 +48,8 @@ func (google *GoogleOAuthService) Init() error {
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
verifier := oauth2.GenerateVerifier() verifier := oauth2.GenerateVerifier()
google.context = ctx google.Context = ctx
google.verifier = verifier google.Verifier = verifier
return nil return nil
} }
@@ -66,24 +64,24 @@ func (oauth *GoogleOAuthService) GenerateState() string {
} }
func (google *GoogleOAuthService) GetAuthURL(state string) string { func (google *GoogleOAuthService) GetAuthURL(state string) string {
return google.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.verifier)) return google.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.Verifier))
} }
func (google *GoogleOAuthService) VerifyCode(code string) error { func (google *GoogleOAuthService) VerifyCode(code string) error {
token, err := google.config.Exchange(google.context, code, oauth2.VerifierOption(google.verifier)) token, err := google.Config.Exchange(google.Context, code, oauth2.VerifierOption(google.Verifier))
if err != nil { if err != nil {
return err return err
} }
google.token = token google.Token = token
return nil return nil
} }
func (google *GoogleOAuthService) Userinfo() (config.Claims, error) { func (google *GoogleOAuthService) Userinfo() (config.Claims, error) {
var user config.Claims var user config.Claims
client := google.config.Client(google.context, google.token) client := google.Config.Client(google.Context, google.Token)
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
if err != nil { if err != nil {
@@ -113,7 +111,3 @@ func (google *GoogleOAuthService) Userinfo() (config.Claims, error) {
return user, nil return user, nil
} }
func (google *GoogleOAuthService) GetName() string {
return google.name
}

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/cenkalti/backoff/v5" "github.com/cenkalti/backoff/v5"
@@ -22,9 +21,8 @@ type LdapServiceConfig struct {
} }
type LdapService struct { type LdapService struct {
Config LdapServiceConfig // exported so as the auth service can use it Config LdapServiceConfig
conn *ldapgo.Conn Conn *ldapgo.Conn
mutex sync.RWMutex
} }
func NewLdapService(config LdapServiceConfig) *LdapService { func NewLdapService(config LdapServiceConfig) *LdapService {
@@ -57,9 +55,6 @@ func (ldap *LdapService) Init() error {
} }
func (ldap *LdapService) connect() (*ldapgo.Conn, error) { func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
ldap.mutex.Lock()
defer ldap.mutex.Unlock()
conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{ conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
InsecureSkipVerify: ldap.Config.Insecure, InsecureSkipVerify: ldap.Config.Insecure,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
@@ -74,7 +69,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
} }
// Set and return the connection // Set and return the connection
ldap.conn = conn ldap.Conn = conn
return conn, nil return conn, nil
} }
@@ -91,10 +86,7 @@ func (ldap *LdapService) Search(username string) (string, error) {
nil, nil,
) )
ldap.mutex.Lock() searchResult, err := ldap.Conn.Search(searchRequest)
defer ldap.mutex.Unlock()
searchResult, err := ldap.conn.Search(searchRequest)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -108,9 +100,7 @@ func (ldap *LdapService) Search(username string) (string, error) {
} }
func (ldap *LdapService) Bind(userDN string, password string) error { func (ldap *LdapService) Bind(userDN string, password string) error {
ldap.mutex.Lock() err := ldap.Conn.Bind(userDN, password)
defer ldap.mutex.Unlock()
err := ldap.conn.Bind(userDN, password)
if err != nil { if err != nil {
return err return err
} }
@@ -128,9 +118,7 @@ func (ldap *LdapService) heartbeat() error {
nil, nil,
) )
ldap.mutex.Lock() _, err := ldap.Conn.Search(searchRequest)
defer ldap.mutex.Unlock()
_, err := ldap.conn.Search(searchRequest)
if err != nil { if err != nil {
return err return err
} }
@@ -149,7 +137,7 @@ func (ldap *LdapService) reconnect() error {
exp.Reset() exp.Reset()
operation := func() (*ldapgo.Conn, error) { operation := func() (*ldapgo.Conn, error) {
ldap.conn.Close() ldap.Conn.Close()
conn, err := ldap.connect() conn, err := ldap.connect()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -5,7 +5,6 @@ import (
"tinyauth/internal/config" "tinyauth/internal/config"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/exp/slices"
) )
type OAuthService interface { type OAuthService interface {
@@ -14,64 +13,62 @@ type OAuthService interface {
GetAuthURL(state string) string GetAuthURL(state string) string
VerifyCode(code string) error VerifyCode(code string) error
Userinfo() (config.Claims, error) Userinfo() (config.Claims, error)
GetName() string
} }
type OAuthBrokerService struct { type OAuthBrokerService struct {
services map[string]OAuthService Services map[string]OAuthService
configs map[string]config.OAuthServiceConfig Configs map[string]config.OAuthServiceConfig
} }
func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService { func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
return &OAuthBrokerService{ return &OAuthBrokerService{
services: make(map[string]OAuthService), Services: make(map[string]OAuthService),
configs: configs, Configs: configs,
} }
} }
func (broker *OAuthBrokerService) Init() error { func (broker *OAuthBrokerService) Init() error {
for name, cfg := range broker.configs { for name, cfg := range broker.Configs {
switch name { switch name {
case "github": case "github":
service := NewGithubOAuthService(cfg) service := NewGithubOAuthService(cfg)
broker.services[name] = service broker.Services[name] = service
case "google": case "google":
service := NewGoogleOAuthService(cfg) service := NewGoogleOAuthService(cfg)
broker.services[name] = service broker.Services[name] = service
default: default:
service := NewGenericOAuthService(cfg) service := NewGenericOAuthService(cfg)
broker.services[name] = service broker.Services[name] = service
} }
} }
for name, service := range broker.services { for name, service := range broker.Services {
err := service.Init() err := service.Init()
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name) log.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name)
return err return err
} }
log.Info().Str("service", service.GetName()).Msg("Initialized OAuth service") log.Info().Msgf("Initialized OAuth service: %s", name)
} }
return nil return nil
} }
func (broker *OAuthBrokerService) GetConfiguredServices() []string { func (broker *OAuthBrokerService) GetConfiguredServices() []string {
services := make([]string, 0, len(broker.services)) services := make([]string, 0, len(broker.Services))
for name := range broker.services { for name := range broker.Services {
services = append(services, name) services = append(services, name)
} }
slices.Sort(services)
return services return services
} }
func (broker *OAuthBrokerService) GetService(name string) (OAuthService, bool) { func (broker *OAuthBrokerService) GetService(name string) (OAuthService, bool) {
service, exists := broker.services[name] service, exists := broker.Services[name]
return service, exists return service, exists
} }
func (broker *OAuthBrokerService) GetUser(service string) (config.Claims, error) { func (broker *OAuthBrokerService) GetUser(service string) (config.Claims, error) {
oauthService, exists := broker.services[service] oauthService, exists := broker.Services[service]
if !exists { if !exists {
return config.Claims{}, errors.New("oauth service not found") return config.Claims{}, errors.New("oauth service not found")
} }

View File

@@ -6,43 +6,32 @@ import (
"net/url" "net/url"
"strings" "strings"
"tinyauth/internal/config" "tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
"maps"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/weppos/publicsuffix-go/publicsuffix"
) )
// Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) // Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
func GetCookieDomain(u string) (string, error) { func GetUpperDomain(appUrl string) (string, error) {
parsed, err := url.Parse(u) appUrlParsed, err := url.Parse(appUrl)
if err != nil { if err != nil {
return "", err return "", err
} }
host := parsed.Hostname() host := appUrlParsed.Hostname()
if netIP := net.ParseIP(host); netIP != nil { if netIP := net.ParseIP(host); netIP != nil {
return "", errors.New("IP addresses not allowed") return "", errors.New("IP addresses are not allowed")
} }
parts := strings.Split(host, ".") urlParts := strings.Split(host, ".")
if len(parts) < 3 { if len(urlParts) < 2 {
return "", errors.New("invalid app url, must be at least second level domain") return "", errors.New("invalid domain, must be at least second level domain")
} }
domain := strings.Join(parts[1:], ".") return strings.Join(urlParts[1:], "."), nil
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil)
if err != nil {
return "", errors.New("domain in public suffix list, cannot set cookies")
}
return domain, nil
} }
func ParseFileToLine(content string) string { func ParseFileToLine(content string) string {
@@ -60,7 +49,6 @@ func ParseFileToLine(content string) string {
} }
func Filter[T any](slice []T, test func(T) bool) (res []T) { func Filter[T any](slice []T, test func(T) bool) (res []T) {
res = make([]T, 0)
for _, value := range slice { for _, value := range slice {
if test(value) { if test(value) {
res = append(res, value) res = append(res, value)
@@ -100,13 +88,13 @@ func IsRedirectSafe(redirectURL string, domain string) bool {
return false return false
} }
cookieDomain, err := GetCookieDomain(redirectURL) upper, err := GetUpperDomain(redirectURL)
if err != nil { if err != nil {
return false return false
} }
if cookieDomain != domain { if upper != domain {
return false return false
} }
@@ -133,67 +121,3 @@ func GetLogLevel(level string) zerolog.Level {
return zerolog.InfoLevel return zerolog.InfoLevel
} }
} }
func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[string]config.OAuthServiceConfig, error) {
providers := make(map[string]config.OAuthServiceConfig)
// Get from environment variables
envMap := make(map[string]string)
for _, e := range env {
pair := strings.SplitN(e, "=", 2)
if len(pair) == 2 {
envMap[pair[0]] = pair[1]
}
}
envProviders, err := decoders.DecodeEnv(envMap)
if err != nil {
return nil, err
}
maps.Copy(providers, envProviders.Providers)
// Get from flags
flagsMap := make(map[string]string)
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "--") {
pair := strings.SplitN(arg[2:], "=", 2)
if len(pair) == 2 {
flagsMap[pair[0]] = pair[1]
}
}
}
flagProviders, err := decoders.DecodeFlags(flagsMap)
if err != nil {
return nil, err
}
maps.Copy(providers, flagProviders.Providers)
// For every provider get correct secret from file if set
for name, provider := range providers {
secret := GetSecret(provider.ClientSecret, provider.ClientSecretFile)
provider.ClientSecret = secret
provider.ClientSecretFile = ""
providers[name] = provider
}
// If we have google/github providers and no redirect URL then set a default
for id := range config.OverrideProviders {
if provider, exists := providers[id]; exists {
if provider.RedirectURL == "" {
provider.RedirectURL = appUrl + "/api/oauth/callback/" + id
providers[id] = provider
}
}
}
// Return combined providers
return providers, nil
}

View File

@@ -1,271 +0,0 @@
package utils_test
import (
"os"
"testing"
"tinyauth/internal/config"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
)
func TestGetRootDomain(t *testing.T) {
// Normal case
domain := "http://sub.tinyauth.app"
expected := "tinyauth.app"
result, err := utils.GetCookieDomain(domain)
assert.NilError(t, err)
assert.Equal(t, expected, result)
// Domain with multiple subdomains
domain = "http://b.c.tinyauth.app"
expected = "c.tinyauth.app"
result, err = utils.GetCookieDomain(domain)
assert.NilError(t, err)
assert.Equal(t, expected, result)
// Domain with no subdomain
domain = "http://tinyauth.app"
expected = "tinyauth.app"
_, err = utils.GetCookieDomain(domain)
assert.Error(t, err, "invalid app url, must be at least second level domain")
// Invalid domain (only TLD)
domain = "com"
_, err = utils.GetCookieDomain(domain)
assert.ErrorContains(t, err, "invalid app url, must be at least second level domain")
// IP address
domain = "http://10.10.10.10"
_, err = utils.GetCookieDomain(domain)
assert.ErrorContains(t, err, "IP addresses not allowed")
// Invalid URL
domain = "http://[::1]:namedport"
_, err = utils.GetCookieDomain(domain)
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
// URL with scheme and path
domain = "https://sub.tinyauth.app/path"
expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain)
assert.NilError(t, err)
assert.Equal(t, expected, result)
// URL with port
domain = "http://sub.tinyauth.app:8080"
expected = "tinyauth.app"
result, err = utils.GetCookieDomain(domain)
assert.NilError(t, err)
assert.Equal(t, expected, result)
// Domain managed by ICANN
domain = "http://example.co.uk"
_, err = utils.GetCookieDomain(domain)
assert.Error(t, err, "domain in public suffix list, cannot set cookies")
}
func TestParseFileToLine(t *testing.T) {
// Normal case
content := "user1\nuser2\nuser3"
expected := "user1,user2,user3"
result := utils.ParseFileToLine(content)
assert.Equal(t, expected, result)
// Case with empty lines and spaces
content = " user1 \n\n user2 \n user3 \n"
expected = "user1,user2,user3"
result = utils.ParseFileToLine(content)
assert.Equal(t, expected, result)
// Case with only empty lines
content = "\n\n\n"
expected = ""
result = utils.ParseFileToLine(content)
assert.Equal(t, expected, result)
// Case with single user
content = "singleuser"
expected = "singleuser"
result = utils.ParseFileToLine(content)
assert.Equal(t, expected, result)
// Case with trailing newline
content = "user1\nuser2\n"
expected = "user1,user2"
result = utils.ParseFileToLine(content)
assert.Equal(t, expected, result)
}
func TestFilter(t *testing.T) {
// Normal case
slice := []int{1, 2, 3, 4, 5}
testFunc := func(n int) bool { return n%2 == 0 }
expected := []int{2, 4}
result := utils.Filter(slice, testFunc)
assert.DeepEqual(t, expected, result)
// Case with no matches
slice = []int{1, 3, 5}
testFunc = func(n int) bool { return n%2 == 0 }
expected = []int{}
result = utils.Filter(slice, testFunc)
assert.DeepEqual(t, expected, result)
// Case with all matches
slice = []int{2, 4, 6}
testFunc = func(n int) bool { return n%2 == 0 }
expected = []int{2, 4, 6}
result = utils.Filter(slice, testFunc)
assert.DeepEqual(t, expected, result)
// Case with empty slice
slice = []int{}
testFunc = func(n int) bool { return n%2 == 0 }
expected = []int{}
result = utils.Filter(slice, testFunc)
assert.DeepEqual(t, expected, result)
// Case with different type (string)
sliceStr := []string{"apple", "banana", "cherry"}
testFuncStr := func(s string) bool { return len(s) > 5 }
expectedStr := []string{"banana", "cherry"}
resultStr := utils.Filter(sliceStr, testFuncStr)
assert.DeepEqual(t, expectedStr, resultStr)
}
func TestGetContext(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(nil)
// Normal case
c.Set("context", &config.UserContext{Username: "testuser"})
result, err := utils.GetContext(c)
assert.NilError(t, err)
assert.Equal(t, "testuser", result.Username)
// Case with no context
c.Set("context", nil)
_, err = utils.GetContext(c)
assert.Error(t, err, "invalid user context in request")
// Case with invalid context type
c.Set("context", "invalid type")
_, err = utils.GetContext(c)
assert.Error(t, err, "invalid user context in request")
}
func TestIsRedirectSafe(t *testing.T) {
// Setup
domain := "example.com"
// Case with no subdomain
redirectURL := "http://example.com/welcome"
result := utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, false, result)
// Case with different domain
redirectURL = "http://malicious.com/phishing"
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, false, result)
// Case with subdomain
redirectURL = "http://sub.example.com/page"
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, true, result)
// Case with empty redirect URL
redirectURL = ""
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, false, result)
// Case with invalid URL
redirectURL = "http://[::1]:namedport"
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, false, result)
// Case with URL having port
redirectURL = "http://sub.example.com:8080/page"
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, true, result)
// Case with URL having different subdomain
redirectURL = "http://another.example.com/page"
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, true, result)
// Case with URL having different TLD
redirectURL = "http://example.org/page"
result = utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, false, result)
}
func TestGetOAuthProvidersConfig(t *testing.T) {
env := []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET=client1-secret"}
args := []string{"/tinyauth/tinyauth", "--providers-client2-client-id=client2-id", "--providers-client2-client-secret=client2-secret"}
expected := map[string]config.OAuthServiceConfig{
"client1": {
ClientID: "client1-id",
ClientSecret: "client1-secret",
},
"client2": {
ClientID: "client2-id",
ClientSecret: "client2-secret",
},
}
result, err := utils.GetOAuthProvidersConfig(env, args, "")
assert.NilError(t, err)
assert.DeepEqual(t, expected, result)
// Case with no providers
env = []string{}
args = []string{"/tinyauth/tinyauth"}
expected = map[string]config.OAuthServiceConfig{}
result, err = utils.GetOAuthProvidersConfig(env, args, "")
assert.NilError(t, err)
assert.DeepEqual(t, expected, result)
// Case with secret from file
file, err := os.Create("/tmp/tinyauth_test_file")
assert.NilError(t, err)
_, err = file.WriteString("file content\n")
assert.NilError(t, err)
err = file.Close()
assert.NilError(t, err)
defer os.Remove("/tmp/tinyauth_test_file")
env = []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET_FILE=/tmp/tinyauth_test_file"}
args = []string{"/tinyauth/tinyauth"}
expected = map[string]config.OAuthServiceConfig{
"client1": {
ClientID: "client1-id",
ClientSecret: "file content",
},
}
result, err = utils.GetOAuthProvidersConfig(env, args, "")
assert.NilError(t, err)
assert.DeepEqual(t, expected, result)
// Case with google provider and no redirect URL
env = []string{"PROVIDERS_GOOGLE_CLIENT_ID=google-id", "PROVIDERS_GOOGLE_CLIENT_SECRET=google-secret"}
args = []string{"/tinyauth/tinyauth"}
expected = map[string]config.OAuthServiceConfig{
"google": {
ClientID: "google-id",
ClientSecret: "google-secret",
RedirectURL: "http://app.url/api/oauth/callback/google",
},
}
result, err = utils.GetOAuthProvidersConfig(env, args, "http://app.url")
assert.NilError(t, err)
assert.DeepEqual(t, expected, result)
}

View File

@@ -1,89 +0,0 @@
package decoders
import (
"reflect"
"strings"
"tinyauth/internal/config"
)
func NormalizeKeys(keys map[string]string, rootName string, sep string) map[string]string {
normalized := make(map[string]string)
knownKeys := getKnownKeys()
for k, v := range keys {
var finalKey []string
var suffix string
var camelClientName string
var camelField string
finalKey = append(finalKey, rootName)
finalKey = append(finalKey, "providers")
lowerKey := strings.ToLower(k)
if !strings.HasPrefix(lowerKey, "providers"+sep) {
continue
}
for _, known := range knownKeys {
if strings.HasSuffix(lowerKey, strings.ReplaceAll(known, "-", sep)) {
suffix = known
break
}
}
if suffix == "" {
continue
}
if strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lowerKey, "providers"+sep), strings.ReplaceAll(suffix, "-", sep))) == "" {
continue
}
clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(lowerKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep)
for i, p := range clientNameParts {
if i == 0 {
camelClientName += p
continue
}
if p == "" {
continue
}
camelClientName += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:])
}
finalKey = append(finalKey, camelClientName)
fieldParts := strings.Split(suffix, "-")
for i, p := range fieldParts {
if i == 0 {
camelField += p
continue
}
if p == "" {
continue
}
camelField += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:])
}
finalKey = append(finalKey, camelField)
normalized[strings.Join(finalKey, ".")] = v
}
return normalized
}
func getKnownKeys() []string {
var known []string
p := config.OAuthServiceConfig{}
v := reflect.ValueOf(p)
typeOfP := v.Type()
for field := range typeOfP.NumField() {
known = append(known, typeOfP.Field(field).Tag.Get("key"))
}
return known
}

View File

@@ -1,49 +0,0 @@
package decoders_test
import (
"testing"
"tinyauth/internal/utils/decoders"
"gotest.tools/v3/assert"
)
func TestNormalizeKeys(t *testing.T) {
// Test with env
test := map[string]string{
"PROVIDERS_CLIENT1_CLIENT_ID": "my-client-id",
"PROVIDERS_CLIENT1_CLIENT_SECRET": "my-client-secret",
"PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID": "my-awesome-client-id",
"PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret",
"I_LOOK_LIKE_A_KEY_CLIENT_ID": "should-not-appear",
"PROVIDERS_CLIENT_ID": "should-not-appear",
}
expected := map[string]string{
"tinyauth.providers.client1.clientId": "my-client-id",
"tinyauth.providers.client1.clientSecret": "my-client-secret",
"tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id",
"tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret",
}
normalized := decoders.NormalizeKeys(test, "tinyauth", "_")
assert.DeepEqual(t, normalized, expected)
// Test with flags (assume -- is already stripped)
test = map[string]string{
"providers-client1-client-id": "my-client-id",
"providers-client1-client-secret": "my-client-secret",
"providers-my-awesome-client-client-id": "my-awesome-client-id",
"providers-my-awesome-client-client-secret-file": "/path/to/secret",
"providers-should-not-appear-client": "should-not-appear",
"i-look-like-a-key-client-id": "should-not-appear",
"providers-client-id": "should-not-appear",
}
expected = map[string]string{
"tinyauth.providers.client1.clientId": "my-client-id",
"tinyauth.providers.client1.clientSecret": "my-client-secret",
"tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id",
"tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret",
}
normalized = decoders.NormalizeKeys(test, "tinyauth", "-")
assert.DeepEqual(t, normalized, expected)
}

View File

@@ -1,20 +0,0 @@
package decoders
import (
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
)
func DecodeEnv(env map[string]string) (config.Providers, error) {
normalized := NormalizeKeys(env, "tinyauth", "_")
var providers config.Providers
err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers")
if err != nil {
return config.Providers{}, err
}
return providers, nil
}

View File

@@ -1,60 +0,0 @@
package decoders_test
import (
"testing"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
"gotest.tools/v3/assert"
)
func TestDecodeEnv(t *testing.T) {
// Variables
expected := config.Providers{
Providers: map[string]config.OAuthServiceConfig{
"client1": {
ClientID: "client1-id",
ClientSecret: "client1-secret",
Scopes: []string{"client1-scope1", "client1-scope2"},
RedirectURL: "client1-redirect-url",
AuthURL: "client1-auth-url",
UserinfoURL: "client1-user-info-url",
Name: "Client1",
InsecureSkipVerify: false,
},
"client2": {
ClientID: "client2-id",
ClientSecret: "client2-secret",
Scopes: []string{"client2-scope1", "client2-scope2"},
RedirectURL: "client2-redirect-url",
AuthURL: "client2-auth-url",
UserinfoURL: "client2-user-info-url",
Name: "My Awesome Client2",
InsecureSkipVerify: false,
},
},
}
test := map[string]string{
"PROVIDERS_CLIENT1_CLIENT_ID": "client1-id",
"PROVIDERS_CLIENT1_CLIENT_SECRET": "client1-secret",
"PROVIDERS_CLIENT1_SCOPES": "client1-scope1,client1-scope2",
"PROVIDERS_CLIENT1_REDIRECT_URL": "client1-redirect-url",
"PROVIDERS_CLIENT1_AUTH_URL": "client1-auth-url",
"PROVIDERS_CLIENT1_USER_INFO_URL": "client1-user-info-url",
"PROVIDERS_CLIENT1_NAME": "Client1",
"PROVIDERS_CLIENT1_INSECURE_SKIP_VERIFY": "false",
"PROVIDERS_CLIENT2_CLIENT_ID": "client2-id",
"PROVIDERS_CLIENT2_CLIENT_SECRET": "client2-secret",
"PROVIDERS_CLIENT2_SCOPES": "client2-scope1,client2-scope2",
"PROVIDERS_CLIENT2_REDIRECT_URL": "client2-redirect-url",
"PROVIDERS_CLIENT2_AUTH_URL": "client2-auth-url",
"PROVIDERS_CLIENT2_USER_INFO_URL": "client2-user-info-url",
"PROVIDERS_CLIENT2_NAME": "My Awesome Client2",
"PROVIDERS_CLIENT2_INSECURE_SKIP_VERIFY": "false",
}
// Test
res, err := decoders.DecodeEnv(test)
assert.NilError(t, err)
assert.DeepEqual(t, expected, res)
}

View File

@@ -1,30 +0,0 @@
package decoders
import (
"strings"
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
)
func DecodeFlags(flags map[string]string) (config.Providers, error) {
filtered := filterFlags(flags)
normalized := NormalizeKeys(filtered, "tinyauth", "-")
var providers config.Providers
err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers")
if err != nil {
return config.Providers{}, err
}
return providers, nil
}
func filterFlags(flags map[string]string) map[string]string {
filtered := make(map[string]string)
for k, v := range flags {
filtered[strings.TrimPrefix(k, "--")] = v
}
return filtered
}

View File

@@ -1,60 +0,0 @@
package decoders_test
import (
"testing"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
"gotest.tools/v3/assert"
)
func TestDecodeFlags(t *testing.T) {
// Variables
expected := config.Providers{
Providers: map[string]config.OAuthServiceConfig{
"client1": {
ClientID: "client1-id",
ClientSecret: "client1-secret",
Scopes: []string{"client1-scope1", "client1-scope2"},
RedirectURL: "client1-redirect-url",
AuthURL: "client1-auth-url",
UserinfoURL: "client1-user-info-url",
Name: "Client1",
InsecureSkipVerify: false,
},
"client2": {
ClientID: "client2-id",
ClientSecret: "client2-secret",
Scopes: []string{"client2-scope1", "client2-scope2"},
RedirectURL: "client2-redirect-url",
AuthURL: "client2-auth-url",
UserinfoURL: "client2-user-info-url",
Name: "My Awesome Client2",
InsecureSkipVerify: false,
},
},
}
test := map[string]string{
"--providers-client1-client-id": "client1-id",
"--providers-client1-client-secret": "client1-secret",
"--providers-client1-scopes": "client1-scope1,client1-scope2",
"--providers-client1-redirect-url": "client1-redirect-url",
"--providers-client1-auth-url": "client1-auth-url",
"--providers-client1-user-info-url": "client1-user-info-url",
"--providers-client1-name": "Client1",
"--providers-client1-insecure-skip-verify": "false",
"--providers-client2-client-id": "client2-id",
"--providers-client2-client-secret": "client2-secret",
"--providers-client2-scopes": "client2-scope1,client2-scope2",
"--providers-client2-redirect-url": "client2-redirect-url",
"--providers-client2-auth-url": "client2-auth-url",
"--providers-client2-user-info-url": "client2-user-info-url",
"--providers-client2-name": "My Awesome Client2",
"--providers-client2-insecure-skip-verify": "false",
}
// Test
res, err := decoders.DecodeFlags(test)
assert.NilError(t, err)
assert.DeepEqual(t, expected, res)
}

View File

@@ -1,19 +0,0 @@
package decoders
import (
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
)
func DecodeLabels(labels map[string]string) (config.Apps, error) {
var appLabels config.Apps
err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps")
if err != nil {
return config.Apps{}, err
}
return appLabels, nil
}

View File

@@ -1,68 +0,0 @@
package decoders_test
import (
"testing"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
"gotest.tools/v3/assert"
)
func TestDecodeLabels(t *testing.T) {
// Variables
expected := config.Apps{
Apps: map[string]config.App{
"foo": {
Config: config.AppConfig{
Domain: "example.com",
},
Users: config.AppUsers{
Allow: "user1,user2",
Block: "user3",
},
OAuth: config.AppOAuth{
Whitelist: "somebody@example.com",
Groups: "group3",
},
IP: config.AppIP{
Allow: []string{"10.71.0.1/24", "10.71.0.2"},
Block: []string{"10.10.10.10", "10.0.0.0/24"},
Bypass: []string{"192.168.1.1"},
},
Response: config.AppResponse{
Headers: []string{"X-Foo=Bar", "X-Baz=Qux"},
BasicAuth: config.AppBasicAuth{
Username: "admin",
Password: "password",
PasswordFile: "/path/to/passwordfile",
},
},
Path: config.AppPath{
Allow: "/public",
Block: "/private",
},
},
},
}
test := map[string]string{
"tinyauth.apps.foo.config.domain": "example.com",
"tinyauth.apps.foo.users.allow": "user1,user2",
"tinyauth.apps.foo.users.block": "user3",
"tinyauth.apps.foo.oauth.whitelist": "somebody@example.com",
"tinyauth.apps.foo.oauth.groups": "group3",
"tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2",
"tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24",
"tinyauth.apps.foo.ip.bypass": "192.168.1.1",
"tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux",
"tinyauth.apps.foo.response.basicauth.username": "admin",
"tinyauth.apps.foo.response.basicauth.password": "password",
"tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile",
"tinyauth.apps.foo.path.allow": "/public",
"tinyauth.apps.foo.path.block": "/private",
}
// Test
result, err := decoders.DecodeLabels(test)
assert.NilError(t, err)
assert.DeepEqual(t, expected, result)
}

View File

@@ -1,31 +0,0 @@
package utils
import (
"os"
"testing"
"gotest.tools/v3/assert"
)
func TestReadFile(t *testing.T) {
// Setup
file, err := os.Create("/tmp/tinyauth_test_file")
assert.NilError(t, err)
_, err = file.WriteString("file content\n")
assert.NilError(t, err)
err = file.Close()
assert.NilError(t, err)
defer os.Remove("/tmp/tinyauth_test_file")
// Normal case
content, err := ReadFile("/tmp/tinyauth_test_file")
assert.NilError(t, err)
assert.Equal(t, "file content\n", content)
// Non-existing file
content, err = ReadFile("/tmp/non_existing_file")
assert.ErrorContains(t, err, "no such file or directory")
assert.Equal(t, "", content)
}

View File

@@ -3,8 +3,22 @@ package utils
import ( import (
"net/http" "net/http"
"strings" "strings"
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
) )
func GetLabels(labels map[string]string) (config.Labels, error) {
var labelsParsed config.Labels
err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip")
if err != nil {
return config.Labels{}, err
}
return labelsParsed, nil
}
func ParseHeaders(headers []string) map[string]string { func ParseHeaders(headers []string) map[string]string {
headerMap := make(map[string]string) headerMap := make(map[string]string)
for _, header := range headers { for _, header := range headers {

View File

@@ -1,87 +0,0 @@
package utils_test
import (
"testing"
"tinyauth/internal/utils"
"gotest.tools/v3/assert"
)
func TestParseHeaders(t *testing.T) {
// Normal case
headers := []string{
"X-Custom-Header=Value",
"Another-Header=AnotherValue",
}
expected := map[string]string{
"X-Custom-Header": "Value",
"Another-Header": "AnotherValue",
}
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
// Case insensitivity and trimming
headers = []string{
" x-custom-header = Value ",
"ANOTHER-HEADER=AnotherValue",
}
expected = map[string]string{
"X-Custom-Header": "Value",
"Another-Header": "AnotherValue",
}
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
// Invalid headers (missing '=', empty key/value)
headers = []string{
"InvalidHeader",
"=NoKey",
"NoValue=",
" = ",
}
expected = map[string]string{}
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
// Headers with unsafe characters
headers = []string{
"X-Custom-Header=Val\x00ue", // Null byte
"Another-Header=Anoth\x7FerValue", // DEL character
"Good-Header=GoodValue",
}
expected = map[string]string{
"X-Custom-Header": "Value",
"Another-Header": "AnotherValue",
"Good-Header": "GoodValue",
}
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
// Header with spaces in key (should be ignored)
headers = []string{
"X Custom Header=Value",
"Valid-Header=ValidValue",
}
expected = map[string]string{
"Valid-Header": "ValidValue",
}
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
}
func TestSanitizeHeader(t *testing.T) {
// Normal case
header := "X-Custom-Header"
expected := "X-Custom-Header"
assert.Equal(t, expected, utils.SanitizeHeader(header))
// Header with unsafe characters
header = "X-Cust\x00om-Hea\x7Fder" // Null byte and DEL character
expected = "X-Custom-Header"
assert.Equal(t, expected, utils.SanitizeHeader(header))
// Header with only unsafe characters
header = "\x00\x01\x02\x7F"
expected = ""
assert.Equal(t, expected, utils.SanitizeHeader(header))
// Header with spaces and tabs (should be preserved)
header = "X Custom\tHeader"
expected = "X Custom\tHeader"
assert.Equal(t, expected, utils.SanitizeHeader(header))
}

View File

@@ -1,13 +1,17 @@
package utils package utils
import ( import (
"bytes"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"errors" "errors"
"io"
"net" "net"
"regexp" "regexp"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/crypto/hkdf"
) )
func GetSecret(conf string, file string) string { func GetSecret(conf string, file string) string {
@@ -45,14 +49,26 @@ func GetBasicAuth(username string, password string) string {
return base64.StdEncoding.EncodeToString([]byte(auth)) return base64.StdEncoding.EncodeToString([]byte(auth))
} }
func FilterIP(filter string, ip string) (bool, error) { func DeriveKey(secret string, info string) (string, error) {
ipAddr := net.ParseIP(ip) hash := sha256.New
hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice
key := make([]byte, 24)
if ipAddr == nil { _, err := io.ReadFull(hkdf, key)
return false, errors.New("invalid IP address") if err != nil {
return "", err
} }
filter = strings.Replace(filter, "-", "/", -1) if bytes.Equal(key, make([]byte, 24)) {
return "", errors.New("derived key is empty")
}
encodedKey := base64.StdEncoding.EncodeToString(key)
return encodedKey, nil
}
func FilterIP(filter string, ip string) (bool, error) {
ipAddr := net.ParseIP(ip)
if strings.Contains(filter, "/") { if strings.Contains(filter, "/") {
_, cidr, err := net.ParseCIDR(filter) _, cidr, err := net.ParseCIDR(filter)
@@ -101,7 +117,8 @@ func CheckFilter(filter string, str string) bool {
return false return false
} }
func GenerateUUID(str string) string { func GenerateIdentifier(str string) string {
uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str))
return uuid.String() uuidString := uuid.String()
return strings.Split(uuidString, "-")[0]
} }

View File

@@ -1,148 +0,0 @@
package utils_test
import (
"os"
"testing"
"tinyauth/internal/utils"
"gotest.tools/v3/assert"
)
func TestGetSecret(t *testing.T) {
// Setup
file, err := os.Create("/tmp/tinyauth_test_secret")
assert.NilError(t, err)
_, err = file.WriteString(" secret \n")
assert.NilError(t, err)
err = file.Close()
assert.NilError(t, err)
defer os.Remove("/tmp/tinyauth_test_secret")
// Get from config
assert.Equal(t, "mysecret", utils.GetSecret("mysecret", ""))
// Get from file
assert.Equal(t, "secret", utils.GetSecret("", "/tmp/tinyauth_test_secret"))
// Get from both (config should take precedence)
assert.Equal(t, "mysecret", utils.GetSecret("mysecret", "/tmp/tinyauth_test_secret"))
// Get from none
assert.Equal(t, "", utils.GetSecret("", ""))
// Get from non-existing file
assert.Equal(t, "", utils.GetSecret("", "/tmp/non_existing_file"))
}
func TestParseSecretFile(t *testing.T) {
// Normal case
content := " mysecret \n"
assert.Equal(t, "mysecret", utils.ParseSecretFile(content))
// Multiple lines (should take the first non-empty line)
content = "\n\n firstsecret \nsecondsecret\n"
assert.Equal(t, "firstsecret", utils.ParseSecretFile(content))
// All empty lines
content = "\n \n \n"
assert.Equal(t, "", utils.ParseSecretFile(content))
// Empty content
content = ""
assert.Equal(t, "", utils.ParseSecretFile(content))
}
func TestGetBasicAuth(t *testing.T) {
// Normal case
username := "user"
password := "pass"
expected := "dXNlcjpwYXNz" // base64 of "user:pass"
assert.Equal(t, expected, utils.GetBasicAuth(username, password))
// Empty username
username = ""
password = "pass"
expected = "OnBhc3M=" // base64 of ":pass"
assert.Equal(t, expected, utils.GetBasicAuth(username, password))
// Empty password
username = "user"
password = ""
expected = "dXNlcjo=" // base64 of "user:"
assert.Equal(t, expected, utils.GetBasicAuth(username, password))
}
func TestFilterIP(t *testing.T) {
// Exact match IPv4
ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1")
assert.NilError(t, err)
assert.Equal(t, true, ok)
// Non-match IPv4
ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2")
assert.NilError(t, err)
assert.Equal(t, false, ok)
// CIDR match IPv4
ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2")
assert.NilError(t, err)
assert.Equal(t, true, ok)
// CIDR match IPv4 with '-' instead of '/'
ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5")
assert.NilError(t, err)
assert.Equal(t, true, ok)
// CIDR non-match IPv4
ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1")
assert.NilError(t, err)
assert.Equal(t, false, ok)
// Invalid CIDR
ok, err = utils.FilterIP("10.10.0.0/222", "10.0.0.1")
assert.ErrorContains(t, err, "invalid CIDR address")
assert.Equal(t, false, ok)
// Invalid IP in filter
ok, err = utils.FilterIP("invalid_ip", "10.5.5.5")
assert.ErrorContains(t, err, "invalid IP address in filter")
assert.Equal(t, false, ok)
// Invalid IP to check
ok, err = utils.FilterIP("10.10.10.10", "invalid_ip")
assert.ErrorContains(t, err, "invalid IP address")
assert.Equal(t, false, ok)
}
func TestCheckFilter(t *testing.T) {
// Empty filter
assert.Equal(t, true, utils.CheckFilter("", "anystring"))
// Exact match
assert.Equal(t, true, utils.CheckFilter("hello", "hello"))
// Regex match
assert.Equal(t, true, utils.CheckFilter("/^h.*o$/", "hello"))
// Invalid regex
assert.Equal(t, false, utils.CheckFilter("/[unclosed", "test"))
// Comma-separated values
assert.Equal(t, true, utils.CheckFilter("apple, banana, cherry", "banana"))
// No match
assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape"))
}
func TestGenerateUUID(t *testing.T) {
// Consistent output for same input
id1 := utils.GenerateUUID("teststring")
id2 := utils.GenerateUUID("teststring")
assert.Equal(t, id1, id2)
// Different output for different input
id3 := utils.GenerateUUID("differentstring")
assert.Assert(t, id1 != id3)
}

View File

@@ -1,50 +0,0 @@
package utils_test
import (
"testing"
"tinyauth/internal/utils"
"gotest.tools/v3/assert"
)
func TestCapitalize(t *testing.T) {
// Test empty string
assert.Equal(t, "", utils.Capitalize(""))
// Test single character
assert.Equal(t, "A", utils.Capitalize("a"))
// Test multiple characters
assert.Equal(t, "Hello", utils.Capitalize("hello"))
// Test already capitalized
assert.Equal(t, "World", utils.Capitalize("World"))
// Test non-alphabetic first character
assert.Equal(t, "1number", utils.Capitalize("1number"))
// Test Unicode characters
assert.Equal(t, "Γειά", utils.Capitalize("γειά"))
assert.Equal(t, "Привет", utils.Capitalize("привет"))
}
func TestCoalesceToString(t *testing.T) {
// Test with []any containing strings
assert.Equal(t, "a,b,c", utils.CoalesceToString([]any{"a", "b", "c"}))
// Test with []any containing mixed types
assert.Equal(t, "a,c", utils.CoalesceToString([]any{"a", 1, "c", true}))
// Test with []any containing no strings
assert.Equal(t, "", utils.CoalesceToString([]any{1, 2, 3}))
// Test with string input
assert.Equal(t, "hello", utils.CoalesceToString("hello"))
// Test with non-string, non-[]any input
assert.Equal(t, "", utils.CoalesceToString(123))
// Test with nil input
assert.Equal(t, "", utils.CoalesceToString(nil))
}

View File

@@ -1,163 +0,0 @@
package utils_test
import (
"os"
"testing"
"tinyauth/internal/utils"
"gotest.tools/v3/assert"
)
func TestGetUsers(t *testing.T) {
// Setup
file, err := os.Create("/tmp/tinyauth_users_test.txt")
assert.NilError(t, err)
_, err = file.WriteString(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G \n user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ") // Spacing is on purpose
assert.NilError(t, err)
err = file.Close()
assert.NilError(t, err)
defer os.Remove("/tmp/tinyauth_users_test.txt")
// Test file
users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt")
assert.NilError(t, err)
assert.Equal(t, 2, len(users))
assert.Equal(t, "user1", users[0].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
assert.Equal(t, "user2", users[1].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
// Test config
users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "")
assert.NilError(t, err)
assert.Equal(t, 2, len(users))
assert.Equal(t, "user3", users[0].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
assert.Equal(t, "user4", users[1].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
// Test both
users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt")
assert.NilError(t, err)
assert.Equal(t, 3, len(users))
assert.Equal(t, "user5", users[0].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
assert.Equal(t, "user1", users[1].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
assert.Equal(t, "user2", users[2].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password)
// Test empty
users, err = utils.GetUsers("", "")
assert.NilError(t, err)
assert.Equal(t, 0, len(users))
// Test non-existent file
users, err = utils.GetUsers("", "/tmp/non_existent_file.txt")
assert.ErrorContains(t, err, "no such file or directory")
assert.Equal(t, 0, len(users))
}
func TestParseUsers(t *testing.T) {
// Valid users
users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP
assert.NilError(t, err)
assert.Equal(t, 2, len(users))
assert.Equal(t, "user1", users[0].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
assert.Equal(t, "", users[0].TotpSecret)
assert.Equal(t, "user2", users[1].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
// Valid weirdly spaced users
users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose
assert.NilError(t, err)
assert.Equal(t, 2, len(users))
assert.Equal(t, "user1", users[0].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
assert.Equal(t, "", users[0].TotpSecret)
assert.Equal(t, "user2", users[1].Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
}
func TestParseUser(t *testing.T) {
// Valid user without TOTP
user, err := utils.ParseUser("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G")
assert.NilError(t, err)
assert.Equal(t, "user1", user.Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password)
assert.Equal(t, "", user.TotpSecret)
// Valid user with TOTP
user, err = utils.ParseUser("user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF")
assert.NilError(t, err)
assert.Equal(t, "user2", user.Username)
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password)
assert.Equal(t, "ABCDEF", user.TotpSecret)
// Valid user with $$ in password
user, err = utils.ParseUser("user3:pa$$word123")
assert.NilError(t, err)
assert.Equal(t, "user3", user.Username)
assert.Equal(t, "pa$word123", user.Password)
assert.Equal(t, "", user.TotpSecret)
// User with spaces
user, err = utils.ParseUser(" user4 : password123 : TOTPSECRET ")
assert.NilError(t, err)
assert.Equal(t, "user4", user.Username)
assert.Equal(t, "password123", user.Password)
assert.Equal(t, "TOTPSECRET", user.TotpSecret)
// Invalid users
_, err = utils.ParseUser("user1") // Missing password
assert.ErrorContains(t, err, "invalid user format")
_, err = utils.ParseUser("user1:")
assert.ErrorContains(t, err, "invalid user format")
_, err = utils.ParseUser(":password123")
assert.ErrorContains(t, err, "invalid user format")
_, err = utils.ParseUser("user1:password123:ABC:EXTRA") // Too many parts
assert.ErrorContains(t, err, "invalid user format")
_, err = utils.ParseUser("user1::ABC")
assert.ErrorContains(t, err, "invalid user format")
_, err = utils.ParseUser(":password123:ABC")
assert.ErrorContains(t, err, "invalid user format")
_, err = utils.ParseUser(" : : ")
assert.ErrorContains(t, err, "invalid user format")
}