Compare commits

...

27 Commits

Author SHA1 Message Date
Stavros
725150f197 refactor: use strcase lib to handle text case conversions 2025-10-26 11:58:36 +02:00
Stavros
ed3b1c49cb refactor: rework decoders logic for cleaner code 2025-10-26 00:35:35 +03:00
Chris Ellrich
c5bb389258 feat: ACL labels from environment variables (#422)
* feat: add LabelService to retrieve application labels from environment variables

* feat: allow usage of labels from docker and env variables simultaneously

Prioritize labels from environment variables over labels from docker
labels

* fix: handle error returned by label_serive.go/LoadLabels

see https://github.com/steveiliop56/tinyauth/pull/422#discussion_r2443443032

* refactor(label_service): use simple loop instead of slices.ContainsFunc to avoid experimental slices package
see https://github.com/steveiliop56/tinyauth/pull/422#pullrequestreview-3354632045

* refactor: merge acl logic into one service

---------

Co-authored-by: Stavros <steveiliop56@gmail.com>
2025-10-21 16:02:31 +03:00
Stavros
6647c6cd78 refactor: use gorm generics api for database actions 2025-10-19 19:16:53 +03:00
Stavros
7231efcbc3 feat: add routine to cleanup expired sessions 2025-10-19 19:10:24 +03:00
Stavros
5482430907 refactor: generate a verifier on every oauth auth session 2025-10-19 19:03:38 +03:00
dependabot[bot]
97639ae903 chore(deps): bump the minor-patch group across 1 directory with 5 updates (#415)
Bumps the minor-patch group with 5 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.2` | `5.90.3` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.0.0` | `16.0.1` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.1` | `19.2.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.46.0` | `8.46.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.9` | `7.1.10` |



Updates `@tanstack/react-query` from 5.90.2 to 5.90.3
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.3/packages/react-query)

Updates `react-i18next` from 16.0.0 to 16.0.1
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.0.0...v16.0.1)

Updates `@types/react-dom` from 19.2.1 to 19.2.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `typescript-eslint` from 8.46.0 to 8.46.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.1/packages/typescript-eslint)

Updates `vite` from 7.1.9 to 7.1.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.10/packages/vite)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-i18next
  dependency-version: 16.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: typescript-eslint
  dependency-version: 8.46.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: vite
  dependency-version: 7.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 19:05:21 +03:00
dependabot[bot]
82350594c1 chore(deps): bump github.com/charmbracelet/huh in the minor-patch group (#414)
Bumps the minor-patch group with 1 update: [github.com/charmbracelet/huh](https://github.com/charmbracelet/huh).


Updates `github.com/charmbracelet/huh` from 0.7.0 to 0.8.0
- [Release notes](https://github.com/charmbracelet/huh/releases)
- [Commits](https://github.com/charmbracelet/huh/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/huh
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 19:04:58 +03:00
Stavros
57b7b66813 fix: let docker meta decide if the version is latest or not 2025-10-14 15:45:50 +03:00
dependabot[bot]
2ea921f3ca chore(deps): bump the minor-patch group in /frontend with 3 updates (#411)
Bumps the minor-patch group in /frontend with 3 updates: [i18next](https://github.com/i18next/i18next), [react-hook-form](https://github.com/react-hook-form/react-hook-form) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `i18next` from 25.5.3 to 25.6.0
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.5.3...v25.6.0)

Updates `react-hook-form` from 7.64.0 to 7.65.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.64.0...v7.65.0)

Updates `@types/node` from 24.7.1 to 24.7.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 25.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.65.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 24.7.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 19:14:30 +03:00
dependabot[bot]
473109b36a chore(deps): bump oven/bun from 1.2.23-alpine to 1.3.0-alpine (#410)
Bumps oven/bun from 1.2.23-alpine to 1.3.0-alpine.

---
updated-dependencies:
- dependency-name: oven/bun
  dependency-version: 1.3.0-alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 19:14:11 +03:00
Scott McKendry
f628d1f0b3 fix(redirect): allow root cookie domain host redirects (#409)
Previously IsRedirectSafe rejected redirects to the exact cookie domain
when AppURL had multiple subdomain levels, because it stripped the first
label twice.
2025-10-13 11:55:43 +03:00
dependabot[bot]
a9c1bf8865 chore(deps): bump github.com/quic-go/quic-go from 0.54.0 to 0.54.1 (#403)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.0 to 0.54.1.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.54.0...v0.54.1)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.54.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-12 11:53:14 +03:00
dependabot[bot]
81136eeb42 chore(deps): bump the minor-patch group across 1 directory with 2 updates (#400)
Bumps the minor-patch group with 2 updates in the /frontend directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `react-router` from 7.9.3 to 7.9.4
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.4/packages/react-router)

Updates `@types/node` from 24.7.0 to 24.7.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 24.7.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-12 11:52:12 +03:00
dependabot[bot]
8ee331a564 chore(deps-dev): bump eslint-plugin-react-hooks in /frontend (#398)
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 6.1.1 to 7.0.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-12 11:51:44 +03:00
dependabot[bot]
0996711f08 chore(deps): bump the minor-patch group with 3 updates (#396)
Bumps the minor-patch group with 3 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [github.com/docker/docker](https://github.com/docker/docker) and [golang.org/x/oauth2](https://github.com/golang/oauth2).


Updates `golang.org/x/crypto` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0)

Updates `github.com/docker/docker` from 28.5.0+incompatible to 28.5.1+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1)

Updates `golang.org/x/oauth2` from 0.31.0 to 0.32.0
- [Commits](https://github.com/golang/oauth2/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.1+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-12 11:51:04 +03:00
Stavros
64222b6d15 fix: don't use container name in label discovery 2025-10-12 11:29:31 +03:00
Stavros
1b87ed9b99 feat: add config dumps to trace log level 2025-10-12 11:29:31 +03:00
Stavros
dc67be2ba0 fix: remove html suffix from docs link 2025-10-11 23:18:20 +03:00
Stavros
9b76a84ee2 feat: add trace logging 2025-10-11 15:27:01 +03:00
Stavros
ed20d2cf51 fix: use correct path for data directory in distroless 2025-10-11 15:18:37 +03:00
Stavros
fc7e395e66 feat: sort configured providers based on name length 2025-10-10 17:16:22 +03:00
Stavros
b940d681c3 feat: use recovery gin middleware in engine 2025-10-10 16:42:19 +03:00
Stavros
a1ec4a69cf fix: remove spaces before checking oauth name and username 2025-10-10 16:28:52 +03:00
github-actions[bot]
4047cea451 docs: regenerate readme sponsors list (#402)
Co-authored-by: GitHub <noreply@github.com>
2025-10-10 15:52:45 +03:00
Stavros
5a4855c12c refactor: move docker connection check to start up 2025-10-10 15:45:04 +03:00
Stavros
05d4dbd68e fix: ensure data directory exists on docker image 2025-10-10 15:34:09 +03:00
30 changed files with 509 additions and 391 deletions

View File

@@ -360,7 +360,6 @@ jobs:
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
latest=true
prefix=v,onlatest=false
tags: |
type=semver,pattern={{version}}
@@ -403,8 +402,8 @@ jobs:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
latest=false
prefix=v,onlatest=false
suffix=-distroless,onlatest=false
prefix=v
suffix=-distroless
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}

View File

@@ -1,5 +1,5 @@
# Site builder
FROM oven/bun:1.2.23-alpine AS frontend-builder
FROM oven/bun:1.3.0-alpine AS frontend-builder
WORKDIR /frontend
@@ -47,6 +47,8 @@ WORKDIR /tinyauth
COPY --from=builder /tinyauth/tinyauth ./
RUN mkdir -p /data
EXPOSE 3000
VOLUME ["/data"]

View File

@@ -1,5 +1,5 @@
# Site builder
FROM oven/bun:1.2.23-alpine AS frontend-builder
FROM oven/bun:1.3.0-alpine AS frontend-builder
WORKDIR /frontend
@@ -38,6 +38,8 @@ COPY ./cmd ./cmd
COPY ./internal ./internal
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN mkdir -p data
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"
# Runner
@@ -47,6 +49,9 @@ WORKDIR /tinyauth
COPY --from=builder /tinyauth/tinyauth ./
# Since it's distroless, we need to copy the data directory from the builder stage
COPY --from=builder /tinyauth/data /data
EXPOSE 3000
VOLUME ["/data"]

View File

@@ -23,7 +23,7 @@ Tinyauth is a simple authentication middleware that adds a simple login screen o
## Getting Started
You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities.
You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities.
## Demo
@@ -53,7 +53,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
A big thank you to the following people for providing me with more coffee:
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<!-- sponsors -->
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<!-- sponsors -->
## Acknowledgements

View File

@@ -2,7 +2,7 @@ root = "/tinyauth"
tmp_dir = "tmp"
[build]
pre_cmd = ["mkdir -p internal/assets/dist", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"]
pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html", "go install github.com/go-delve/delve/cmd/dlv@v1.25.0"]
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ."
bin = "/go/bin/dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
include_ext = ["go"]

View File

@@ -112,6 +112,10 @@ func (c *rootCmd) run(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel)))
log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth")
if log.Logger.GetLevel() == zerolog.TraceLevel {
log.Warn().Msg("Log level set to trace, this will log sensitive information!")
}
app := bootstrap.NewBootstrapApp(conf)
err = app.Setup()

View File

@@ -10,11 +10,11 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query": "^5.90.3",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.5.3",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
@@ -22,10 +22,10 @@
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.64.0",
"react-i18next": "^16.0.0",
"react-hook-form": "^7.65.0",
"react-i18next": "^16.0.1",
"react-markdown": "^10.1.0",
"react-router": "^7.9.3",
"react-router": "^7.9.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.14",
@@ -34,19 +34,19 @@
"devDependencies": {
"@eslint/js": "^9.37.0",
"@tanstack/eslint-plugin-query": "^5.91.0",
"@types/node": "^24.7.0",
"@types/node": "^24.7.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0",
"prettier": "3.6.2",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9",
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10",
},
},
},
@@ -329,9 +329,9 @@
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-Kn6yWyRe3dIPf7NqyDMhcsTBz2Oh8jPSOpBdlnLQhGBJ6iTMBFYA4B1UreGJ/WdfzQskSMh5imcyWF+wqa/Q5g=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.3", "", {}, "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.3", "", { "dependencies": { "@tanstack/query-core": "5.90.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q=="],
"@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=="],
@@ -355,33 +355,33 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="],
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="],
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/type-utils": "8.46.0", "@typescript-eslint/utils": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.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.46.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/type-utils": "8.46.1", "@typescript-eslint/utils": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.0", "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0" } }, "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.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-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1", "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-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.45.0", "", {}, "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.0", "@typescript-eslint/tsconfig-utils": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.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-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -493,7 +493,7 @@
"eslint": ["eslint@9.37.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.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@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-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.0", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="],
@@ -575,11 +575,15 @@
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"i18next": ["i18next@25.5.3", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg=="],
"i18next": ["i18next@25.6.0", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
@@ -787,9 +791,9 @@
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-hook-form": ["react-hook-form@7.64.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg=="],
"react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="],
"react-i18next": ["react-i18next@16.0.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q=="],
"react-i18next": ["react-i18next@16.0.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug=="],
"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=="],
@@ -799,7 +803,7 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="],
"react-router": ["react-router@7.9.4", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA=="],
"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=="],
@@ -867,7 +871,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.46.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.0", "@typescript-eslint/parser": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw=="],
"typescript-eslint": ["typescript-eslint@8.46.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA=="],
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
@@ -895,7 +899,7 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
"vite": ["vite@7.1.9", "", { "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-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
"vite": ["vite@7.1.10", "", { "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-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
@@ -967,25 +971,25 @@
"@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.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -993,9 +997,7 @@
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.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-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"eslint-plugin-react-hooks/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -1011,7 +1013,7 @@
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="],
"@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=="],
@@ -1027,11 +1029,11 @@
"@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.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
@@ -1045,9 +1047,9 @@
"@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.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="],
"@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=="],

View File

@@ -16,11 +16,11 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query": "^5.90.3",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.5.3",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
@@ -28,10 +28,10 @@
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.64.0",
"react-i18next": "^16.0.0",
"react-hook-form": "^7.65.0",
"react-i18next": "^16.0.1",
"react-markdown": "^10.1.0",
"react-router": "^7.9.3",
"react-router": "^7.9.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.14",
@@ -40,18 +40,18 @@
"devDependencies": {
"@eslint/js": "^9.37.0",
"@tanstack/eslint-plugin-query": "^5.91.0",
"@types/node": "^24.7.0",
"@types/node": "^24.7.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0",
"prettier": "3.6.2",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9"
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10"
}
}

29
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/spf13/viper v1.21.0
github.com/traefik/paerser v0.2.2
github.com/weppos/publicsuffix-go v0.50.0
golang.org/x/crypto v0.42.0
golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
gorm.io/gorm v1.31.0
gotest.tools/v3 v3.5.2
@@ -45,17 +45,18 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/quic-go/quic-go v0.54.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/tools v0.37.0 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
@@ -71,16 +72,16 @@ require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.0+incompatible
github.com/docker/docker v28.5.1+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -130,10 +131,10 @@ require (
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.31.0
golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.32.0
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

62
go.sum
View File

@@ -12,8 +12,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -27,18 +27,18 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
@@ -72,8 +72,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E=
github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -229,8 +229,8 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
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=
@@ -259,6 +259,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -304,32 +306,32 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
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=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=

View File

@@ -2,21 +2,25 @@ package bootstrap
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"sort"
"strings"
"time"
"tinyauth/internal/config"
"tinyauth/internal/controller"
"tinyauth/internal/middleware"
"tinyauth/internal/model"
"tinyauth/internal/service"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type Controller interface {
@@ -74,6 +78,15 @@ func (app *BootstrapApp) Setup() error {
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
// Dumps
log.Trace().Interface("config", app.config).Msg("Config dump")
log.Trace().Interface("users", users).Msg("Users dump")
log.Trace().Interface("oauthProviders", oauthProviders).Msg("OAuth providers dump")
log.Trace().Str("cookieDomain", cookieDomain).Msg("Cookie domain")
log.Trace().Str("sessionCookieName", sessionCookieName).Msg("Session cookie name")
log.Trace().Str("csrfCookieName", csrfCookieName).Msg("CSRF cookie name")
log.Trace().Str("redirectCookieName", redirectCookieName).Msg("Redirect cookie name")
// Create configs
authConfig := service.AuthServiceConfig{
Users: users,
@@ -126,12 +139,14 @@ func (app *BootstrapApp) Setup() error {
// Create services
dockerService := service.NewDockerService()
aclsService := service.NewAccessControlsService(dockerService)
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
// Initialize services
// Initialize services (order matters)
services := []Service{
dockerService,
aclsService,
authService,
oauthBrokerService,
}
@@ -157,6 +172,10 @@ func (app *BootstrapApp) Setup() error {
})
}
sort.Slice(configuredProviders, func(i, j int) bool {
return configuredProviders[i].Name < configuredProviders[j].Name
})
if authService.UserAuthConfigured() || ldapService != nil {
configuredProviders = append(configuredProviders, controller.Provider{
Name: "Username",
@@ -173,6 +192,7 @@ func (app *BootstrapApp) Setup() error {
// Create engine
engine := gin.New()
engine.Use(gin.Recovery())
if len(app.config.TrustedProxies) > 0 {
err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ","))
@@ -228,7 +248,7 @@ func (app *BootstrapApp) Setup() error {
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL,
}, apiRouter, dockerService, authService)
}, apiRouter, aclsService, authService)
userController := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: cookieDomain,
@@ -262,6 +282,10 @@ func (app *BootstrapApp) Setup() error {
go app.heartbeat()
}
// Start DB cleanup routine
log.Debug().Msg("Starting database cleanup routine")
go app.dbCleanup(database)
// Start server
address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port)
log.Info().Msgf("Starting server on %s", address)
@@ -323,3 +347,17 @@ func (app *BootstrapApp) heartbeat() {
}
}
}
func (app *BootstrapApp) dbCleanup(db *gorm.DB) {
ticker := time.NewTicker(time.Duration(30) * time.Minute)
defer ticker.Stop()
ctx := context.Background()
for ; true; <-ticker.C {
log.Debug().Msg("Cleaning up old database sessions")
_, err := gorm.G[model.Session](db).Where("expiry < ?", time.Now().UnixMilli()).Delete(ctx)
if err != nil {
log.Error().Err(err).Msg("Failed to cleanup old sessions")
}
}
}

View File

@@ -53,16 +53,16 @@ type Claims struct {
}
type OAuthServiceConfig struct {
ClientID string `key:"client-id"`
ClientSecret string `key:"client-secret"`
ClientSecretFile string `key:"client-secret-file"`
Scopes []string `key:"scopes"`
RedirectURL string `key:"redirect-url"`
AuthURL string `key:"auth-url"`
TokenURL string `key:"token-url"`
UserinfoURL string `key:"user-info-url"`
InsecureSkipVerify bool `key:"insecure-skip-verify"`
Name string `key:"name"`
ClientID string `field:"client-id"`
ClientSecret string
ClientSecretFile string
Scopes []string
RedirectURL string `field:"redirect-url"`
AuthURL string `field:"auth-url"`
TokenURL string `field:"token-url"`
UserinfoURL string `field:"user-info-url"`
InsecureSkipVerify bool
Name string
}
var OverrideProviders = map[string]string{

View File

@@ -72,6 +72,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
service.GenerateVerifier()
state := service.GenerateState()
authURL := service.GetAuthURL(state)
c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
@@ -162,7 +163,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
var name string
if user.Name != "" {
if strings.TrimSpace(user.Name) != "" {
log.Debug().Msg("Using name from OAuth provider")
name = user.Name
} else {
@@ -172,7 +173,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
var username string
if user.PreferredUsername != "" {
if strings.TrimSpace(user.PreferredUsername) != "" {
log.Debug().Msg("Using preferred username from OAuth provider")
username = user.PreferredUsername
} else {

View File

@@ -24,15 +24,15 @@ type ProxyControllerConfig struct {
type ProxyController struct {
config ProxyControllerConfig
router *gin.RouterGroup
docker *service.DockerService
acls *service.AccessControlsService
auth *service.AuthService
}
func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController {
func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, acls *service.AccessControlsService, auth *service.AuthService) *ProxyController {
return &ProxyController{
config: config,
router: router,
docker: docker,
acls: acls,
auth: auth,
}
}
@@ -76,18 +76,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host")
labels, err := controller.docker.GetLabels(host)
// Get acls
acls, err := controller.acls.GetAccessControls(host)
if err != nil {
log.Error().Err(err).Msg("Failed to get labels from Docker")
log.Error().Err(err).Msg("Failed to get access controls for resource")
controller.handleError(c, req, isBrowser)
return
}
log.Trace().Interface("acls", acls).Msg("ACLs for resource")
clientIP := c.ClientIP()
if controller.auth.IsBypassedIP(labels.IP, clientIP) {
controller.setHeaders(c, labels)
if controller.auth.IsBypassedIP(acls.IP, clientIP) {
controller.setHeaders(c, acls)
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
@@ -95,7 +98,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return
}
authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path)
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
if err != nil {
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
@@ -105,7 +108,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
if !authEnabled {
log.Debug().Msg("Authentication disabled for resource, allowing access")
controller.setHeaders(c, labels)
controller.setHeaders(c, acls)
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
@@ -113,7 +116,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return
}
if !controller.auth.CheckIP(labels.IP, clientIP) {
if !controller.auth.CheckIP(acls.IP, clientIP) {
if req.Proxy == "nginx" || !isBrowser {
c.JSON(401, gin.H{
"status": 401,
@@ -150,13 +153,15 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
userContext = context
}
log.Trace().Interface("context", userContext).Msg("User context from request")
if userContext.Provider == "basic" && userContext.TotpEnabled {
log.Debug().Msg("User has TOTP enabled, denying basic auth access")
userContext.IsLoggedIn = false
}
if userContext.IsLoggedIn {
appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels)
appAllowed := controller.auth.IsResourceAllowed(c, userContext, acls)
if !appAllowed {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
@@ -190,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
if userContext.OAuth {
groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups)
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
if !groupOK {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
@@ -230,7 +235,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
controller.setHeaders(c, labels)
controller.setHeaders(c, acls)
c.JSON(200, gin.H{
"status": 200,
@@ -260,21 +265,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode()))
}
func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) {
func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
c.Header("Authorization", c.Request.Header.Get("Authorization"))
headers := utils.ParseHeaders(labels.Response.Headers)
headers := utils.ParseHeaders(acls.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)
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.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)))
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
log.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
}
}

View File

@@ -39,6 +39,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
assert.NilError(t, dockerService.Init())
// Access controls
accessControlsService := service.NewAccessControlsService(dockerService)
assert.NilError(t, accessControlsService.Init())
// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{
@@ -59,7 +64,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
// Controller
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: "http://localhost:8080",
}, group, dockerService, authService)
}, group, accessControlsService, authService)
ctrl.SetupRoutes()
return router, recorder, authService

View File

@@ -0,0 +1,103 @@
package service
import (
"os"
"strings"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
"github.com/rs/zerolog/log"
)
type AccessControlsService struct {
docker *DockerService
envACLs config.Apps
}
func NewAccessControlsService(docker *DockerService) *AccessControlsService {
return &AccessControlsService{
docker: docker,
}
}
func (acls *AccessControlsService) Init() error {
acls.envACLs = config.Apps{}
env := os.Environ()
appEnvVars := []string{}
for _, e := range env {
if strings.HasPrefix(e, "TINYAUTH_APPS_") {
appEnvVars = append(appEnvVars, e)
}
}
err := acls.loadEnvACLs(appEnvVars)
if err != nil {
return err
}
return nil
}
func (acls *AccessControlsService) loadEnvACLs(appEnvVars []string) error {
if len(appEnvVars) == 0 {
return nil
}
envAcls := map[string]string{}
for _, e := range appEnvVars {
parts := strings.SplitN(e, "=", 2)
if len(parts) != 2 {
continue
}
// Normalize key, this should use the same normalization logic as in utils/decoders/decoders.go
key := parts[0]
key = strings.ToLower(key)
key = strings.ReplaceAll(key, "_", ".")
value := parts[1]
envAcls[key] = value
}
apps, err := decoders.DecodeLabels(envAcls)
if err != nil {
return err
}
acls.envACLs = apps
return nil
}
func (acls *AccessControlsService) lookupEnvACLs(appDomain string) *config.App {
if len(acls.envACLs.Apps) == 0 {
return nil
}
for appName, appACLs := range acls.envACLs.Apps {
if appACLs.Config.Domain == appDomain {
return &appACLs
}
if strings.SplitN(appDomain, ".", 2)[0] == appName {
return &appACLs
}
}
return nil
}
func (acls *AccessControlsService) GetAccessControls(appDomain string) (config.App, error) {
// First check environment variables
envACLs := acls.lookupEnvACLs(appDomain)
if envACLs != nil {
log.Debug().Str("domain", appDomain).Msg("Found matching access controls in environment variables")
return *envACLs, nil
}
// Fallback to Docker labels
return acls.docker.GetLabels(appDomain)
}

View File

@@ -1,6 +1,8 @@
package service
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
@@ -41,6 +43,7 @@ type AuthService struct {
loginMutex sync.RWMutex
ldap *LdapService
database *gorm.DB
ctx context.Context
}
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService {
@@ -54,6 +57,7 @@ func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapS
}
func (auth *AuthService) Init() error {
auth.ctx = context.Background()
return nil
}
@@ -213,7 +217,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
OAuthName: data.OAuthName,
}
err = auth.database.Create(&session).Error
err = gorm.G[model.Session](auth.database).Create(auth.ctx, &session)
if err != nil {
return err
@@ -231,10 +235,10 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
return err
}
res := auth.database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{})
_, err = gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).Delete(auth.ctx)
if res.Error != nil {
return res.Error
if err != nil {
return err
}
c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
@@ -249,15 +253,13 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
return config.SessionCookie{}, err
}
var session model.Session
session, err := gorm.G[model.Session](auth.database).Where("uuid = ?", cookie).First(auth.ctx)
res := auth.database.Unscoped().Where("uuid = ?", cookie).First(&session)
if res.Error != nil {
return config.SessionCookie{}, res.Error
if err != nil {
return config.SessionCookie{}, err
}
if res.RowsAffected == 0 {
if errors.Is(err, gorm.ErrRecordNotFound) {
return config.SessionCookie{}, fmt.Errorf("session not found")
}
@@ -287,21 +289,21 @@ func (auth *AuthService) UserAuthConfigured() bool {
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) IsResourceAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
if context.OAuth {
log.Debug().Msg("Checking OAuth whitelist")
return utils.CheckFilter(labels.OAuth.Whitelist, context.Email)
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
}
if labels.Users.Block != "" {
if acls.Users.Block != "" {
log.Debug().Msg("Checking blocked users")
if utils.CheckFilter(labels.Users.Block, context.Username) {
if utils.CheckFilter(acls.Users.Block, context.Username) {
return false
}
}
log.Debug().Msg("Checking users")
return utils.CheckFilter(labels.Users.Allow, context.Username)
return utils.CheckFilter(acls.Users.Allow, context.Username)
}
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
@@ -318,6 +320,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
log.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
return true
}
}
@@ -368,8 +371,8 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
}
}
func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
for _, blocked := range labels.Block {
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
for _, blocked := range acls.Block {
res, err := utils.FilterIP(blocked, ip)
if err != nil {
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
@@ -381,7 +384,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
}
}
for _, allowed := range labels.Allow {
for _, allowed := range acls.Allow {
res, err := utils.FilterIP(allowed, ip)
if err != nil {
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
@@ -393,7 +396,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
}
}
if len(labels.Allow) > 0 {
if len(acls.Allow) > 0 {
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
return false
}
@@ -402,8 +405,8 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
return true
}
func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool {
for _, bypassed := range labels.Bypass {
func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
for _, bypassed := range acls.Bypass {
res, err := utils.FilterIP(bypassed, ip)
if err != nil {
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")

View File

@@ -12,8 +12,9 @@ import (
)
type DockerService struct {
client *client.Client
context context.Context
client *client.Client
context context.Context
isConnected bool
}
func NewDockerService() *DockerService {
@@ -31,10 +32,24 @@ func (docker *DockerService) Init() error {
docker.client = client
docker.context = ctx
_, err = docker.client.Ping(docker.context)
if err != nil {
log.Debug().Err(err).Msg("Docker not connected")
docker.isConnected = false
docker.client = nil
docker.context = nil
return nil
}
docker.isConnected = true
log.Debug().Msg("Docker connected")
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{})
if err != nil {
return nil, err
@@ -42,7 +57,7 @@ func (docker *DockerService) GetContainers() ([]container.Summary, error) {
return containers, nil
}
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)
if err != nil {
return container.InspectResponse{}, err
@@ -50,26 +65,19 @@ func (docker *DockerService) InspectContainer(containerId string) (container.Ins
return inspect, nil
}
func (docker *DockerService) DockerConnected() bool {
_, err := docker.client.Ping(docker.context)
return err == nil
}
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
isConnected := docker.DockerConnected()
if !isConnected {
if !docker.isConnected {
log.Debug().Msg("Docker not connected, returning empty labels")
return config.App{}, nil
}
containers, err := docker.GetContainers()
containers, err := docker.getContainers()
if err != nil {
return config.App{}, err
}
for _, ctr := range containers {
inspect, err := docker.InspectContainer(ctr.ID)
inspect, err := docker.inspectContainer(ctr.ID)
if err != nil {
return config.App{}, err
}
@@ -81,12 +89,12 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
for appName, appLabels := range labels.Apps {
if appLabels.Config.Domain == appDomain {
log.Debug().Str("id", inspect.ID).Msg("Found matching container by domain")
log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
return appLabels, nil
}
if strings.TrimPrefix(inspect.Name, "/") == appName {
log.Debug().Str("id", inspect.ID).Msg("Found matching container by app name")
if strings.SplitN(appDomain, ".", 2)[0] == appName {
log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
return appLabels, nil
}
}

View File

@@ -12,6 +12,7 @@ import (
"time"
"tinyauth/internal/config"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
)
@@ -58,10 +59,8 @@ func (generic *GenericOAuthService) Init() error {
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
verifier := oauth2.GenerateVerifier()
generic.context = ctx
generic.verifier = verifier
return nil
}
@@ -75,6 +74,12 @@ func (generic *GenericOAuthService) GenerateState() string {
return state
}
func (generic *GenericOAuthService) GenerateVerifier() string {
verifier := oauth2.GenerateVerifier()
generic.verifier = verifier
return verifier
}
func (generic *GenericOAuthService) GetAuthURL(state string) string {
return generic.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.verifier))
}
@@ -110,6 +115,8 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
return user, err
}
log.Trace().Str("body", string(body)).Msg("Userinfo response body")
err = json.Unmarshal(body, &user)
if err != nil {
return user, err

View File

@@ -53,10 +53,7 @@ func (github *GithubOAuthService) Init() error {
httpClient := &http.Client{}
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
verifier := oauth2.GenerateVerifier()
github.context = ctx
github.verifier = verifier
return nil
}
@@ -70,6 +67,12 @@ func (github *GithubOAuthService) GenerateState() string {
return state
}
func (github *GithubOAuthService) GenerateVerifier() string {
verifier := oauth2.GenerateVerifier()
github.verifier = verifier
return verifier
}
func (github *GithubOAuthService) GetAuthURL(state string) string {
return github.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.verifier))
}

View File

@@ -48,10 +48,7 @@ func (google *GoogleOAuthService) Init() error {
httpClient := &http.Client{}
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
verifier := oauth2.GenerateVerifier()
google.context = ctx
google.verifier = verifier
return nil
}
@@ -65,6 +62,12 @@ func (oauth *GoogleOAuthService) GenerateState() string {
return state
}
func (google *GoogleOAuthService) GenerateVerifier() string {
verifier := oauth2.GenerateVerifier()
google.verifier = verifier
return verifier
}
func (google *GoogleOAuthService) GetAuthURL(state string) string {
return google.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.verifier))
}

View File

@@ -11,6 +11,7 @@ import (
type OAuthService interface {
Init() error
GenerateState() string
GenerateVerifier() string
GetAuthURL(state string) string
VerifyCode(code string) error
Userinfo() (config.Claims, error)

View File

@@ -100,17 +100,17 @@ func IsRedirectSafe(redirectURL string, domain string) bool {
return false
}
cookieDomain, err := GetCookieDomain(redirectURL)
host := parsedURL.Hostname()
if host == domain {
return true
}
cookieDomain, err := GetCookieDomain(redirectURL)
if err != nil {
return false
}
if cookieDomain != domain {
return false
}
return true
return cookieDomain == domain
}
func GetLogLevel(level string) zerolog.Level {
@@ -147,7 +147,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st
}
}
envProviders, err := decoders.DecodeEnv(envMap)
envProviders, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](envMap, "providers")
if err != nil {
return nil, err
@@ -167,7 +167,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st
}
}
flagProviders, err := decoders.DecodeFlags(flagsMap)
flagProviders, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flagsMap, "providers")
if err != nil {
return nil, err

View File

@@ -164,7 +164,7 @@ func TestIsRedirectSafe(t *testing.T) {
// Case with no subdomain
redirectURL := "http://example.com/welcome"
result := utils.IsRedirectSafe(redirectURL, domain)
assert.Equal(t, false, result)
assert.Equal(t, true, result)
// Case with different domain
redirectURL = "http://malicious.com/phishing"
@@ -202,6 +202,41 @@ func TestIsRedirectSafe(t *testing.T) {
assert.Equal(t, false, result)
}
func TestIsRedirectSafeMultiLevel(t *testing.T) {
// Setup
cookieDomain := "tinyauth.example.com"
// Case with 3rd level domain
redirectURL := "http://tinyauth.example.com/welcome"
result := utils.IsRedirectSafe(redirectURL, cookieDomain)
assert.Equal(t, true, result)
// Case with root domain
redirectURL = "http://example.com/unsafe"
result = utils.IsRedirectSafe(redirectURL, cookieDomain)
assert.Equal(t, false, result)
// Case with 4th level domain
redirectURL = "http://auth.tinyauth.example.com/post-login"
result = utils.IsRedirectSafe(redirectURL, cookieDomain)
assert.Equal(t, true, result)
// Case with 5th level domain (should be unsafe)
redirectURL = "http://x.auth.tinyauth.example.com/deep"
result = utils.IsRedirectSafe(redirectURL, cookieDomain)
assert.Equal(t, false, result)
// Case with different subdomain
redirectURL = "http://auth.tinyauth.example.net/attack"
result = utils.IsRedirectSafe(redirectURL, cookieDomain)
assert.Equal(t, false, result)
// Case with malformed URL
redirectURL = "http://[::1]:namedport"
result = utils.IsRedirectSafe(redirectURL, cookieDomain)
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"}

View File

@@ -3,29 +3,24 @@ package decoders
import (
"reflect"
"strings"
"tinyauth/internal/config"
"github.com/stoewer/go-strcase"
)
func NormalizeKeys(keys map[string]string, rootName string, sep string) map[string]string {
func normalizeKeys[T any](input map[string]string, root string, sep string) map[string]string {
knownKeys := getKnownKeys[T]()
normalized := make(map[string]string)
knownKeys := getKnownKeys()
for k, v := range keys {
var finalKey []string
var suffix string
var camelClientName string
var camelField string
for k, v := range input {
parts := []string{"tinyauth"}
finalKey = append(finalKey, rootName)
finalKey = append(finalKey, "providers")
lowerKey := strings.ToLower(k)
key := strings.ToLower(k)
key = strings.ReplaceAll(key, sep, "-")
if !strings.HasPrefix(lowerKey, "providers"+sep) {
continue
}
suffix := ""
for _, known := range knownKeys {
if strings.HasSuffix(lowerKey, strings.ReplaceAll(known, "-", sep)) {
if strings.HasSuffix(key, known) {
suffix = known
break
}
@@ -35,55 +30,47 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri
continue
}
if strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lowerKey, "providers"+sep), strings.ReplaceAll(suffix, "-", sep))) == "" {
parts = append(parts, root)
id := strings.TrimPrefix(key, root+"-")
id = strings.TrimSuffix(id, "-"+suffix)
if id == "" {
continue
}
clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(lowerKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep)
parts = append(parts, id)
parts = append(parts, suffix)
for i, p := range clientNameParts {
if i == 0 {
camelClientName += p
continue
final := ""
for i, part := range parts {
if i > 0 {
final += "."
}
if p == "" {
continue
}
camelClientName += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:])
final += strcase.LowerCamelCase(part)
}
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
normalized[final] = v
}
return normalized
}
func getKnownKeys() []string {
var known []string
func getKnownKeys[T any]() []string {
var keys []string
var t T
p := config.OAuthServiceConfig{}
v := reflect.ValueOf(p)
typeOfP := v.Type()
v := reflect.ValueOf(t)
typeOfT := v.Type()
for field := range typeOfP.NumField() {
known = append(known, typeOfP.Field(field).Tag.Get("key"))
for field := range typeOfT.NumField() {
if typeOfT.Field(field).Tag.Get("field") != "" {
keys = append(keys, typeOfT.Field(field).Tag.Get("field"))
continue
}
keys = append(keys, strcase.KebabCase(typeOfT.Field(field).Name))
}
return known
return keys
}

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 +1,19 @@
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
func DecodeEnv[T any, C any](env map[string]string, subName string) (T, error) {
var result T
err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers")
normalized := normalizeKeys[C](env, subName, "_")
err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName)
if err != nil {
return config.Providers{}, err
return result, err
}
return providers, nil
return result, nil
}

View File

@@ -9,52 +9,29 @@ import (
)
func TestDecodeEnv(t *testing.T) {
// Variables
// Setup
env := map[string]string{
"PROVIDERS_GOOGLE_CLIENT_ID": "google-client-id",
"PROVIDERS_GOOGLE_CLIENT_SECRET": "google-client-secret",
"PROVIDERS_MY_GITHUB_CLIENT_ID": "github-client-id",
"PROVIDERS_MY_GITHUB_CLIENT_SECRET": "github-client-secret",
}
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,
"google": {
ClientID: "google-client-id",
ClientSecret: "google-client-secret",
},
"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,
"myGithub": {
ClientID: "github-client-id",
ClientSecret: "github-client-secret",
},
},
}
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)
// Execute
result, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](env, "providers")
assert.NilError(t, err)
assert.DeepEqual(t, expected, res)
assert.DeepEqual(t, result, expected)
}

View File

@@ -2,23 +2,23 @@ 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
func DecodeFlags[T any, C any](flags map[string]string, subName string) (T, error) {
var result T
err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers")
filtered := filterFlags(flags)
normalized := normalizeKeys[C](filtered, subName, "_")
err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName)
if err != nil {
return config.Providers{}, err
return result, err
}
return providers, nil
return result, nil
}
func filterFlags(flags map[string]string) map[string]string {

View File

@@ -9,52 +9,29 @@ import (
)
func TestDecodeFlags(t *testing.T) {
// Variables
// Setup
flags := map[string]string{
"--providers-google-client-id": "google-client-id",
"--providers-google-client-secret": "google-client-secret",
"--providers-my-github-client-id": "github-client-id",
"--providers-my-github-client-secret": "github-client-secret",
}
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,
"google": {
ClientID: "google-client-id",
ClientSecret: "google-client-secret",
},
"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,
"myGithub": {
ClientID: "github-client-id",
ClientSecret: "github-client-secret",
},
},
}
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)
// Execute
result, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flags, "providers")
assert.NilError(t, err)
assert.DeepEqual(t, expected, res)
assert.DeepEqual(t, result, expected)
}