Compare commits

..

41 Commits

Author SHA1 Message Date
Stavros
a874ef5ba6 chore: remove submodules from release images and workflows 2026-04-10 17:15:23 +03:00
Stavros
5ce19f58d1 refactor: use own paerser library instead of traefik 2026-04-10 17:10:28 +03:00
Stavros
8b91ce09bd refactor: use zod for oidc params (#771)
* refactor: use zod for oidc params

* fix: review comments

* fix: use min instead of nonempty
2026-04-10 16:05:22 +03:00
dependabot[bot]
298f1bf8eb chore(deps): bump oven/bun from 1.3.11-alpine to 1.3.12-alpine (#778)
Bumps oven/bun from 1.3.11-alpine to 1.3.12-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 16:04:59 +03:00
dependabot[bot]
195f788293 chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp (#775)
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.34.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.34.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.43.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 16:04:02 +03:00
dependabot[bot]
02fb35f992 chore(deps): bump peter-evans/create-pull-request from 7 to 8 (#759)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7 to 8.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v7...v8)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  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>
2026-04-10 16:02:49 +03:00
Stavros
2c1b62f464 feat: preserve oidc params in oauth flow (#772) 2026-04-10 15:58:31 +03:00
Scott McKendry
646e24d98c feat(oidc): support access token in body for user info post (#769) 2026-04-08 09:54:54 +01:00
Scott McKendry
0d286d1864 feat(oidc): add post route for /userinfo (#767)
easy two-liner to pass `oidcc-userinfo-post-header` test in conformance
suite.
2026-04-07 23:28:38 +01:00
Stavros
165197e472 feat: add pkce support to oidc server (#766)
* feat: add pkce support to oidc server

* tests: add test cases for pkce

* fix: review comments

* chore: remove debug line

* chore: remove simple logger from testing

* tests: add test for invalid challenge method

* chore: fix typo
2026-04-07 19:04:20 +03:00
github-actions[bot]
431cd33053 docs: regenerate readme sponsors list (#765)
Co-authored-by: GitHub <noreply@github.com>
2026-04-06 14:39:15 +03:00
Stavros
3373dcc412 test: extend traefik browser tests 2026-04-02 18:46:38 +03:00
Stavros
9d666dc108 fix: skip browser detection for nginx and envoy 2026-04-02 18:24:38 +03:00
dependabot[bot]
7ad13935a5 chore(deps): bump docker/build-push-action from 6 to 7 (#749)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  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>
2026-04-02 15:37:35 +03:00
dependabot[bot]
98e788b1e8 chore(deps): bump docker/metadata-action from 5 to 6 (#750)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  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>
2026-04-02 15:37:19 +03:00
dependabot[bot]
a074efb3a3 chore(deps): bump actions/download-artifact from 4 to 8 (#751)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  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>
2026-04-02 15:37:02 +03:00
dependabot[bot]
48ef8c0e4c chore(deps): bump actions/stale from 9 to 10 (#752)
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  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>
2026-04-02 15:36:37 +03:00
dependabot[bot]
1313e8767a chore(deps): bump actions/checkout from 4 to 6 (#753)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  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>
2026-04-02 15:36:23 +03:00
Stavros
892097dc4d fix: account for proxy type in browser response 2026-04-02 15:35:55 +03:00
dependabot[bot]
6542e1b121 chore(deps): bump codecov/codecov-action from 5 to 6 (#746)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  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>
2026-04-01 19:09:28 +03:00
dependabot[bot]
e1d7fa2eb3 chore(deps): bump docker/setup-buildx-action from 3 to 4 (#747)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  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>
2026-04-01 19:09:02 +03:00
dependabot[bot]
41244080c0 chore(deps): bump docker/login-action from 3 to 4 (#748)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  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>
2026-04-01 19:08:48 +03:00
dependabot[bot]
34f9724866 chore(deps): bump actions/upload-artifact from 4 to 7 (#745)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  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>
2026-04-01 19:08:33 +03:00
dependabot[bot]
19a317dd7c chore(deps): bump actions/setup-go from 5 to 6 (#744)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  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>
2026-04-01 19:08:01 +03:00
Stavros
8a9ffcf185 chore: add github action updates in dependabot 2026-04-01 19:03:05 +03:00
Stavros
fc1d4f2082 refactor: use better ignore paths in context middleware (#743) 2026-04-01 17:07:14 +03:00
Stavros
08e6b84615 fix: use int for status in healthcheck cmd 2026-04-01 16:12:11 +03:00
Stavros
cec0a7327a New translations en.json (Ukrainian) (#740) 2026-04-01 15:43:23 +03:00
dependabot[bot]
d7d6a476bf chore(deps): bump the minor-patch group across 1 directory with 5 updates (#742)
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.95.2` | `5.96.1` |
| [i18next](https://github.com/i18next/i18next) | `26.0.2` | `26.0.3` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.1` | `17.0.2` |
| [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.95.2` | `5.96.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.2` | `8.58.0` |



Updates `@tanstack/react-query` from 5.95.2 to 5.96.1
- [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.96.1/packages/react-query)

Updates `i18next` from 26.0.2 to 26.0.3
- [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/v26.0.2...v26.0.3)

Updates `react-i18next` from 17.0.1 to 17.0.2
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.1...v17.0.2)

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

Updates `typescript-eslint` from 8.57.2 to 8.58.0
- [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.58.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.96.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: i18next
  dependency-version: 26.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-i18next
  dependency-version: 17.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/eslint-plugin-query"
  dependency-version: 5.96.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  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>
2026-04-01 15:42:58 +03:00
dependabot[bot]
c3636784b6 chore(deps): bump github.com/go-jose/go-jose/v4 in the minor-patch group (#741)
Bumps the minor-patch group with 1 update: [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose).


Updates `github.com/go-jose/go-jose/v4` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.4
  dependency-type: direct:production
  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>
2026-04-01 15:42:20 +03:00
Stavros
da247f8552 fix: handle oauth provider id mismatch correctly 2026-03-30 23:02:20 +03:00
github-actions[bot]
ce581a76f1 docs: regenerate readme sponsors list (#738)
Co-authored-by: GitHub <noreply@github.com>
2026-03-30 19:33:13 +03:00
Stavros
7139ad1cc6 fix: use correct go image in docker build 2026-03-30 19:21:50 +03:00
dependabot[bot]
7bfee6efc2 chore(deps): bump github.com/charmbracelet/huh from 0.8.0 to 1.0.0 (#708)
* chore(deps): bump github.com/charmbracelet/huh from 0.8.0 to 1.0.0

Bumps [github.com/charmbracelet/huh](https://github.com/charmbracelet/huh) from 0.8.0 to 1.0.0.
- [Release notes](https://github.com/charmbracelet/huh/releases)
- [Commits](https://github.com/charmbracelet/huh/compare/v0.8.0...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/huh
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: breaking changes for huh form

* chore: bump go version everywhere

* chore: go mod tidy

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stavros <steveiliop56@gmail.com>
2026-03-30 19:15:30 +03:00
dependabot[bot]
bf4e567a4e chore(deps): bump lucide-react from 0.577.0 to 1.7.0 in /frontend (#736)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.577.0 to 1.7.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.7.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.7.0
  dependency-type: direct:production
  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>
2026-03-30 19:07:02 +03:00
dependabot[bot]
1926ad9085 chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /frontend (#734)
* chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /frontend

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: remove base url from ts config

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stavros <steveiliop56@gmail.com>
2026-03-30 19:04:34 +03:00
dependabot[bot]
66ceadf974 chore(deps): bump the minor-patch group with 2 updates (#732)
Bumps the minor-patch group with 2 updates: [github.com/rs/zerolog](https://github.com/rs/zerolog) and [modernc.org/sqlite](https://gitlab.com/cznic/sqlite).


Updates `github.com/rs/zerolog` from 1.34.0 to 1.35.0
- [Commits](https://github.com/rs/zerolog/compare/v1.34.0...v1.35.0)

Updates `modernc.org/sqlite` from 1.47.0 to 1.48.0
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.47.0...v1.48.0)

---
updated-dependencies:
- dependency-name: github.com/rs/zerolog
  dependency-version: 1.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: modernc.org/sqlite
  dependency-version: 1.48.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>
2026-03-30 18:49:39 +03:00
dependabot[bot]
45276d6f4c chore(deps): bump axios in /frontend in the minor-patch group (#733)
Bumps the minor-patch group in /frontend with 1 update: [axios](https://github.com/axios/axios).


Updates `axios` from 1.13.6 to 1.14.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.6...v1.14.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.14.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>
2026-03-30 18:48:34 +03:00
dependabot[bot]
90766694f0 chore(deps): bump react-i18next from 16.6.2 to 17.0.1 in /frontend (#735)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 16.6.2 to 17.0.1.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.6.2...v17.0.1)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.1
  dependency-type: direct:production
  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>
2026-03-30 16:24:07 +03:00
dependabot[bot]
68ddd22aed chore(deps): bump i18next from 25.10.5 to 26.0.1 in /frontend (#737)
Bumps [i18next](https://github.com/i18next/i18next) from 25.10.5 to 26.0.1.
- [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.10.5...v26.0.1)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.0.1
  dependency-type: direct:production
  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>
2026-03-30 16:22:47 +03:00
Stavros
5811218dbf refactor: tests (#731)
* tests: rework tests for context controller

* tests: add tests for health controller

* tests: add tests for oidc controller

* tests: use testify assert in context and health controller

* tests: add tests for user controller

* tests: add tests for resources controller

* tests: add well known controller tests

* test: add proxy controller tests

* chore: review comments

* chore: more review comments

* chore: cancel lockdown in testing

* tests: fix get cookie domain tests

* chore: add comment for testing passwords
2026-03-30 15:31:34 +03:00
63 changed files with 2677 additions and 1788 deletions

View File

@@ -24,3 +24,8 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@@ -10,24 +10,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Setup bun - name: Setup bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Setup go - name: Setup go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1.24.0" go-version: "^1.26.0"
- name: Initialize submodules - name: Go dependencies
run: | run: go mod download
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
@@ -56,6 +50,6 @@ jobs:
run: go test -coverprofile=coverage.txt -v ./... run: go test -coverprofile=coverage.txt -v ./...
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v6
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Delete old release - name: Delete old release
run: gh release delete --cleanup-tag --yes nightly || echo release not found run: gh release delete --cleanup-tag --yes nightly || echo release not found
@@ -33,7 +33,7 @@ jobs:
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
@@ -51,7 +51,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
@@ -59,18 +59,9 @@ jobs:
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install go - name: Install go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1.24.0" go-version: "^1.26.0"
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
@@ -94,7 +85,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: tinyauth-amd64 name: tinyauth-amd64
path: tinyauth-amd64 path: tinyauth-amd64
@@ -106,7 +97,7 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
@@ -114,18 +105,9 @@ jobs:
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install go - name: Install go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1.24.0" go-version: "^1.26.0"
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
@@ -149,7 +131,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: tinyauth-arm64 name: tinyauth-arm64
path: tinyauth-arm64 path: tinyauth-arm64
@@ -161,37 +143,28 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/amd64 platforms: linux/amd64
@@ -213,7 +186,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-linux-amd64 name: digests-linux-amd64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -228,37 +201,28 @@ jobs:
- image-build - image-build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/amd64 platforms: linux/amd64
@@ -281,7 +245,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-distroless-linux-amd64 name: digests-distroless-linux-amd64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -295,37 +259,28 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/arm64 platforms: linux/arm64
@@ -347,7 +302,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-linux-arm64 name: digests-linux-arm64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -362,37 +317,28 @@ jobs:
- image-build-arm - image-build-arm
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: nightly ref: nightly
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/arm64 platforms: linux/arm64
@@ -415,7 +361,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-distroless-linux-arm64 name: digests-distroless-linux-arm64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -429,25 +375,25 @@ jobs:
- image-build-arm - image-build-arm
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
path: ${{ runner.temp }}/digests path: ${{ runner.temp }}/digests
pattern: digests-* pattern: digests-*
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: | flavor: |
@@ -468,25 +414,25 @@ jobs:
- image-build-arm-distroless - image-build-arm-distroless
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
path: ${{ runner.temp }}/digests path: ${{ runner.temp }}/digests
pattern: digests-distroless-* pattern: digests-distroless-*
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: | flavor: |
@@ -506,7 +452,7 @@ jobs:
- binary-build - binary-build
- binary-build-arm - binary-build-arm
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
pattern: tinyauth-* pattern: tinyauth-*
path: binaries path: binaries

View File

@@ -14,7 +14,7 @@ jobs:
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Generate metadata - name: Generate metadata
id: metadata id: metadata
@@ -29,24 +29,15 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install bun - name: Install bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install go - name: Install go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1.24.0" go-version: "^1.26.0"
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
@@ -70,7 +61,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: tinyauth-amd64 name: tinyauth-amd64
path: tinyauth-amd64 path: tinyauth-amd64
@@ -81,24 +72,15 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install bun - name: Install bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install go - name: Install go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1.24.0" go-version: "^1.26.0"
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
@@ -122,7 +104,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: tinyauth-arm64 name: tinyauth-arm64
path: tinyauth-arm64 path: tinyauth-arm64
@@ -133,35 +115,26 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/amd64 platforms: linux/amd64
@@ -183,7 +156,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-linux-amd64 name: digests-linux-amd64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -197,35 +170,26 @@ jobs:
- image-build - image-build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/amd64 platforms: linux/amd64
@@ -248,7 +212,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-distroless-linux-amd64 name: digests-distroless-linux-amd64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -261,35 +225,26 @@ jobs:
- generate-metadata - generate-metadata
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/arm64 platforms: linux/arm64
@@ -311,7 +266,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-linux-arm64 name: digests-linux-arm64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -325,35 +280,26 @@ jobs:
- image-build-arm - image-build-arm
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Initialize submodules
run: |
git submodule init
git submodule update
- name: Apply patches
run: |
git apply --directory paerser/ patches/nested_maps.diff
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
id: build id: build
with: with:
platforms: linux/arm64 platforms: linux/arm64
@@ -376,7 +322,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}" touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: digests-distroless-linux-arm64 name: digests-distroless-linux-arm64
path: ${{ runner.temp }}/digests/* path: ${{ runner.temp }}/digests/*
@@ -390,25 +336,25 @@ jobs:
- image-build-arm - image-build-arm
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
path: ${{ runner.temp }}/digests path: ${{ runner.temp }}/digests
pattern: digests-* pattern: digests-*
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: | flavor: |
@@ -431,25 +377,25 @@ jobs:
- image-build-arm-distroless - image-build-arm-distroless
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
path: ${{ runner.temp }}/digests path: ${{ runner.temp }}/digests
pattern: digests-distroless-* pattern: digests-distroless-*
merge-multiple: true merge-multiple: true
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: | flavor: |
@@ -473,7 +419,7 @@ jobs:
- binary-build - binary-build
- binary-build-arm - binary-build-arm
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
pattern: tinyauth-* pattern: tinyauth-*
path: binaries path: binaries

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Generate Sponsors - name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1 uses: JamesIves/github-sponsors-readme-action@v1
@@ -18,7 +18,7 @@ jobs:
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;' template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: | commit-message: |

View File

@@ -7,7 +7,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v10
with: with:
days-before-stale: 30 days-before-stale: 30
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale. stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "paerser"]
path = paerser
url = https://github.com/traefik/paerser
ignore = all

View File

@@ -19,26 +19,9 @@ git clone https://github.com/steveiliop56/tinyauth
cd tinyauth cd tinyauth
``` ```
## Initialize Submodules ## Installing Dependencies
The project uses Git submodules for some dependencies, so you need to initialize them with: While development occurs within Docker, installing the dependencies locally is recommended to avoid import errors. Install the Go dependencies:
```sh
git submodule init
git submodule update
```
## Apply patches
Some of the dependencies must be patched in order to work correctly with the project, you can apply the patches by running:
```sh
git apply --directory paerser/ patches/nested_maps.diff
```
## Installing Requirements
While development occurs within Docker, installing the requirements locally is recommended to avoid import errors. Install the Go dependencies:
```sh ```sh
go mod tidy go mod tidy

View File

@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM oven/bun:1.3.11-alpine AS frontend-builder FROM oven/bun:1.3.12-alpine AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
RUN bun run build RUN bun run build
# Builder # Builder
FROM golang:1.25-alpine3.21 AS builder FROM golang:1.26-alpine3.23 AS builder
ARG VERSION ARG VERSION
ARG COMMIT_HASH ARG COMMIT_HASH
@@ -28,8 +28,6 @@ ARG BUILD_TIMESTAMP
WORKDIR /tinyauth WORKDIR /tinyauth
COPY ./paerser ./paerser
COPY go.mod ./ COPY go.mod ./
COPY go.sum ./ COPY go.sum ./

View File

@@ -1,9 +1,7 @@
FROM golang:1.25-alpine3.21 FROM golang:1.26-alpine3.23
WORKDIR /tinyauth WORKDIR /tinyauth
COPY ./paerser ./paerser
COPY go.mod ./ COPY go.mod ./
COPY go.sum ./ COPY go.sum ./

View File

@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM oven/bun:1.3.11-alpine AS frontend-builder FROM oven/bun:1.3.12-alpine AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
RUN bun run build RUN bun run build
# Builder # Builder
FROM golang:1.25-alpine3.21 AS builder FROM golang:1.26-alpine3.23 AS builder
ARG VERSION ARG VERSION
ARG COMMIT_HASH ARG COMMIT_HASH
@@ -28,8 +28,6 @@ ARG BUILD_TIMESTAMP
WORKDIR /tinyauth WORKDIR /tinyauth
COPY ./paerser ./paerser
COPY go.mod ./ COPY go.mod ./
COPY go.sum ./ COPY go.sum ./

View File

@@ -58,7 +58,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
A big thank you to the following people for providing me with more coffee: A big thank you to the following people for providing me with more coffee:
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#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;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/algorist-ahmad"><img src="https:&#x2F;&#x2F;github.com&#x2F;algorist-ahmad.png" width="64px" alt="User avatar: algorist-ahmad" /></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;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/ax-mad"><img src="https:&#x2F;&#x2F;github.com&#x2F;ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a>&nbsp;&nbsp;<a href="https://github.com/stegratech"><img src="https:&#x2F;&#x2F;github.com&#x2F;stegratech.png" width="64px" alt="User avatar: stegratech" /></a>&nbsp;&nbsp;<!-- sponsors -->
## Acknowledgements ## Acknowledgements

View File

@@ -8,7 +8,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/steveiliop56/tinyauth/internal/utils" "github.com/steveiliop56/tinyauth/internal/utils"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
) )
func createOidcClientCmd() *cli.Command { func createOidcClientCmd() *cli.Command {

View File

@@ -5,9 +5,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/charmbracelet/huh" "charm.land/huh/v2"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -61,9 +61,8 @@ func createUserCmd() *cli.Command {
), ),
) )
var baseTheme *huh.Theme = huh.ThemeBase() theme := new(themeBase)
err := form.WithTheme(theme).Run()
err := form.WithTheme(baseTheme).Run()
if err != nil { if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err) return fmt.Errorf("failed to run interactive prompt: %w", err)

View File

@@ -9,10 +9,10 @@ import (
"github.com/steveiliop56/tinyauth/internal/utils" "github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/charmbracelet/huh" "charm.land/huh/v2"
"github.com/mdp/qrterminal/v3" "github.com/mdp/qrterminal/v3"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
) )
type GenerateTotpConfig struct { type GenerateTotpConfig struct {
@@ -54,9 +54,8 @@ func generateTotpCmd() *cli.Command {
), ),
) )
var baseTheme *huh.Theme = huh.ThemeBase() theme := new(themeBase)
err := form.WithTheme(theme).Run()
err := form.WithTheme(baseTheme).Run()
if err != nil { if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err) return fmt.Errorf("failed to run interactive prompt: %w", err)

View File

@@ -10,11 +10,11 @@ import (
"time" "time"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
) )
type healthzResponse struct { type healthzResponse struct {
Status string `json:"status"` Status int `json:"status"`
Message string `json:"message"` Message string `json:"message"`
} }

View File

@@ -3,13 +3,14 @@ package main
import ( import (
"fmt" "fmt"
"charm.land/huh/v2"
"github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/loaders" "github.com/steveiliop56/tinyauth/internal/utils/loaders"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
) )
func main() { func main() {
@@ -123,3 +124,9 @@ func runCmd(cfg config.Config) error {
return nil return nil
} }
type themeBase struct{}
func (t *themeBase) Theme(isDark bool) *huh.Styles {
return huh.ThemeBase(isDark)
}

View File

@@ -7,9 +7,9 @@ import (
"github.com/steveiliop56/tinyauth/internal/utils" "github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/charmbracelet/huh" "charm.land/huh/v2"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -71,9 +71,8 @@ func verifyUserCmd() *cli.Command {
), ),
) )
var baseTheme *huh.Theme = huh.ThemeBase() theme := new(themeBase)
err := form.WithTheme(theme).Run()
err := form.WithTheme(baseTheme).Run()
if err != nil { if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err) return fmt.Errorf("failed to run interactive prompt: %w", err)

View File

@@ -5,7 +5,7 @@ import (
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
) )
func versionCmd() *cli.Command { func versionCmd() *cli.Command {

View File

@@ -12,21 +12,21 @@
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.96.1",
"axios": "^1.13.6", "axios": "^1.14.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^25.10.5", "i18next": "^26.0.3",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.577.0", "lucide-react": "^1.7.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-hook-form": "^7.72.0", "react-hook-form": "^7.72.0",
"react-i18next": "^16.6.2", "react-i18next": "^17.0.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.13.2", "react-router": "^7.13.2",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@@ -36,7 +36,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tanstack/eslint-plugin-query": "^5.95.2", "@tanstack/eslint-plugin-query": "^5.96.1",
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -48,8 +48,8 @@
"prettier": "3.8.1", "prettier": "3.8.1",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~6.0.2",
"typescript-eslint": "^8.57.2", "typescript-eslint": "^8.58.0",
"vite": "^8.0.3", "vite": "^8.0.3",
}, },
}, },
@@ -369,11 +369,11 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.95.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A=="], "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.96.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-BDJU+Q+zESjarSSFmbzpCBh+1wDxwW+DyQlvwIukF24MHYOoRPH4ouJRTlDdbp3BnIkeylZaHHSgIvxY9lgI/g=="],
"@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="], "@tanstack/query-core": ["@tanstack/query-core@5.96.1", "", {}, "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="], "@tanstack/react-query": ["@tanstack/react-query@5.96.1", "", { "dependencies": { "@tanstack/query-core": "5.96.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
@@ -401,25 +401,25 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -439,7 +439,7 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], "axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
@@ -611,7 +611,7 @@
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"i18next": ["i18next@25.10.5", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w=="], "i18next": ["i18next@26.0.3", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
@@ -697,7 +697,7 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], "lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -805,7 +805,7 @@
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -817,7 +817,7 @@
"react-hook-form": ["react-hook-form@7.72.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw=="], "react-hook-form": ["react-hook-form@7.72.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw=="],
"react-i18next": ["react-i18next@16.6.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA=="], "react-i18next": ["react-i18next@17.0.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA=="],
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
@@ -881,7 +881,7 @@
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -889,9 +889,9 @@
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
"typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="], "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
@@ -1015,31 +1015,31 @@
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
@@ -1061,7 +1061,7 @@
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
"vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
@@ -1079,23 +1079,25 @@
"@babel/helper-module-transforms/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@babel/helper-module-transforms/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/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

@@ -18,21 +18,21 @@
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.96.1",
"axios": "^1.13.6", "axios": "^1.14.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^25.10.5", "i18next": "^26.0.3",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.577.0", "lucide-react": "^1.7.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-hook-form": "^7.72.0", "react-hook-form": "^7.72.0",
"react-i18next": "^16.6.2", "react-i18next": "^17.0.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.13.2", "react-router": "^7.13.2",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@@ -42,7 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tanstack/eslint-plugin-query": "^5.95.2", "@tanstack/eslint-plugin-query": "^5.96.1",
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -54,8 +54,8 @@
"prettier": "3.8.1", "prettier": "3.8.1",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~6.0.2",
"typescript-eslint": "^8.57.2", "typescript-eslint": "^8.58.0",
"vite": "^8.0.3" "vite": "^8.0.3"
} }
} }

View File

@@ -1,55 +1,40 @@
export type OIDCValues = { import { z } from "zod";
scope: string;
response_type: string;
client_id: string;
redirect_uri: string;
state: string;
nonce: string;
};
interface IuseOIDCParams { export const oidcParamsSchema = z.object({
values: OIDCValues; scope: z.string().min(1),
compiled: string; response_type: z.string().min(1),
client_id: z.string().min(1),
redirect_uri: z.string().min(1),
state: z.string().optional(),
nonce: z.string().optional(),
code_challenge: z.string().optional(),
code_challenge_method: z.string().optional(),
});
export const useOIDCParams = (
params: URLSearchParams,
): {
values: z.infer<typeof oidcParamsSchema>;
issues: string[];
isOidc: boolean; isOidc: boolean;
missingParams: string[]; compiled: string;
} } => {
const obj = Object.fromEntries(params.entries());
const parsed = oidcParamsSchema.safeParse(obj);
const optionalParams: string[] = ["state", "nonce"]; if (parsed.success) {
return {
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams { values: parsed.data,
let compiled: string = ""; issues: [],
let isOidc = false; isOidc: true,
const missingParams: string[] = []; compiled: new URLSearchParams(parsed.data).toString(),
};
const values: OIDCValues = {
scope: params.get("scope") ?? "",
response_type: params.get("response_type") ?? "",
client_id: params.get("client_id") ?? "",
redirect_uri: params.get("redirect_uri") ?? "",
state: params.get("state") ?? "",
nonce: params.get("nonce") ?? "",
};
for (const key of Object.keys(values)) {
if (!values[key as keyof OIDCValues]) {
if (!optionalParams.includes(key)) {
missingParams.push(key);
}
}
}
if (missingParams.length === 0) {
isOidc = true;
}
if (isOidc) {
compiled = new URLSearchParams(values).toString();
} }
return { return {
values, issues: parsed.error.issues.map((issue) => issue.path.toString()),
compiled, values: {} as z.infer<typeof oidcParamsSchema>,
isOidc, isOidc: false,
missingParams, compiled: "",
}; };
} };

View File

@@ -57,9 +57,9 @@
"fieldRequired": "Це поле обов'язкове для заповнення", "fieldRequired": "Це поле обов'язкове для заповнення",
"invalidInput": "Невірне введення", "invalidInput": "Невірне введення",
"domainWarningTitle": "Невірний домен", "domainWarningTitle": "Невірний домен",
"domainWarningSubtitle": "Даний ресурс налаштований для доступу з <code>{{appUrl}}</code>, але використовується <code>{{currentUrl}}</code>. Якщо ви продовжите, можуть виникнути проблеми з автентифікацією.", "domainWarningSubtitle": "Ви отримуєте доступ до даного екземпляра з неправильного домену. Якщо ви продовжите, у вас можуть виникнути проблеми з автентифікацією.",
"domainWarningCurrent": "Current:", "domainWarningCurrent": "Поточний:",
"domainWarningExpected": "Expected:", "domainWarningExpected": "Очікувалося:",
"ignoreTitle": "Ігнорувати", "ignoreTitle": "Ігнорувати",
"goToCorrectDomainTitle": "Перейти за коректним доменом", "goToCorrectDomainTitle": "Перейти за коректним доменом",
"authorizeTitle": "Авторизуватись", "authorizeTitle": "Авторизуватись",

View File

@@ -72,36 +72,27 @@ export const AuthorizePage = () => {
const scopeMap = createScopeMap(t); const scopeMap = createScopeMap(t);
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const { const oidcParams = useOIDCParams(searchParams);
values: props,
missingParams,
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);
const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
const getClientInfo = useQuery({ const getClientInfo = useQuery({
queryKey: ["client", props.client_id], queryKey: ["client", oidcParams.values.client_id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/api/oidc/clients/${props.client_id}`); const res = await fetch(
`/api/oidc/clients/${encodeURIComponent(oidcParams.values.client_id)}`,
);
const data = await getOidcClientInfoSchema.parseAsync(await res.json()); const data = await getOidcClientInfoSchema.parseAsync(await res.json());
return data; return data;
}, },
enabled: isOidc, enabled: oidcParams.isOidc,
}); });
const authorizeMutation = useMutation({ const authorizeMutation = useMutation({
mutationFn: () => { mutationFn: () => {
return axios.post("/api/oidc/authorize", { return axios.post("/api/oidc/authorize", {
scope: props.scope, ...oidcParams.values,
response_type: props.response_type,
client_id: props.client_id,
redirect_uri: props.redirect_uri,
state: props.state,
nonce: props.nonce,
}); });
}, },
mutationKey: ["authorize", props.client_id], mutationKey: ["authorize", oidcParams.values.client_id],
onSuccess: (data) => { onSuccess: (data) => {
toast.info(t("authorizeSuccessTitle"), { toast.info(t("authorizeSuccessTitle"), {
description: t("authorizeSuccessSubtitle"), description: t("authorizeSuccessSubtitle"),
@@ -115,17 +106,17 @@ export const AuthorizePage = () => {
}, },
}); });
if (missingParams.length > 0) { if (oidcParams.issues.length > 0) {
return ( return (
<Navigate <Navigate
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`} to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: oidcParams.issues.join(", ") }))}`}
replace replace
/> />
); );
} }
if (!isLoggedIn) { if (!isLoggedIn) {
return <Navigate to={`/login?${compiledOIDCParams}`} replace />; return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
} }
if (getClientInfo.isLoading) { if (getClientInfo.isLoading) {
@@ -152,6 +143,9 @@ export const AuthorizePage = () => {
); );
} }
const scopes =
oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || [];
return ( return (
<Card> <Card>
<CardHeader className="mb-2"> <CardHeader className="mb-2">

View File

@@ -51,15 +51,12 @@ export const LoginPage = () => {
const formId = useId(); const formId = useId();
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const { const redirectUri = searchParams.get("redirect_uri") || undefined;
values: props, const oidcParams = useOIDCParams(searchParams);
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState( const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauthAutoRedirect) !== providers.find((provider) => provider.id === oauthAutoRedirect) !==
undefined && props.redirect_uri, undefined && redirectUri !== undefined,
); );
const oauthProviders = providers.filter( const oauthProviders = providers.filter(
@@ -76,10 +73,18 @@ export const LoginPage = () => {
isPending: oauthIsPending, isPending: oauthIsPending,
variables: oauthVariables, variables: oauthVariables,
} = useMutation({ } = useMutation({
mutationFn: (provider: string) => mutationFn: (provider: string) => {
axios.get( const getParams = function (): string {
`/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, if (oidcParams.isOidc) {
), return `?${oidcParams.compiled}`;
}
if (redirectUri) {
return `?redirect_uri=${encodeURIComponent(redirectUri)}`;
}
return "";
};
return axios.get(`/api/oauth/url/${provider}${getParams()}`);
},
mutationKey: ["oauth"], mutationKey: ["oauth"],
onSuccess: (data) => { onSuccess: (data) => {
toast.info(t("loginOauthSuccessTitle"), { toast.info(t("loginOauthSuccessTitle"), {
@@ -109,8 +114,12 @@ export const LoginPage = () => {
mutationKey: ["login"], mutationKey: ["login"],
onSuccess: (data) => { onSuccess: (data) => {
if (data.data.totpPending) { if (data.data.totpPending) {
if (oidcParams.isOidc) {
window.location.replace(`/totp?${oidcParams.compiled}`);
return;
}
window.location.replace( window.location.replace(
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, `/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
); );
return; return;
} }
@@ -120,12 +129,12 @@ export const LoginPage = () => {
}); });
redirectTimer.current = window.setTimeout(() => { redirectTimer.current = window.setTimeout(() => {
if (isOidc) { if (oidcParams.isOidc) {
window.location.replace(`/authorize?${compiledOIDCParams}`); window.location.replace(`/authorize?${oidcParams.compiled}`);
return; return;
} }
window.location.replace( window.location.replace(
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, `/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
); );
}, 500); }, 500);
}, },
@@ -144,7 +153,7 @@ export const LoginPage = () => {
!isLoggedIn && !isLoggedIn &&
isOauthAutoRedirect && isOauthAutoRedirect &&
!hasAutoRedirectedRef.current && !hasAutoRedirectedRef.current &&
props.redirect_uri redirectUri !== undefined
) { ) {
hasAutoRedirectedRef.current = true; hasAutoRedirectedRef.current = true;
oauthMutate(oauthAutoRedirect); oauthMutate(oauthAutoRedirect);
@@ -155,7 +164,7 @@ export const LoginPage = () => {
hasAutoRedirectedRef, hasAutoRedirectedRef,
oauthAutoRedirect, oauthAutoRedirect,
isOauthAutoRedirect, isOauthAutoRedirect,
props.redirect_uri, redirectUri,
]); ]);
useEffect(() => { useEffect(() => {
@@ -170,14 +179,14 @@ export const LoginPage = () => {
}; };
}, [redirectTimer, redirectButtonTimer]); }, [redirectTimer, redirectButtonTimer]);
if (isLoggedIn && isOidc) { if (isLoggedIn && oidcParams.isOidc) {
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />; return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
} }
if (isLoggedIn && props.redirect_uri !== "") { if (isLoggedIn && redirectUri !== undefined) {
return ( return (
<Navigate <Navigate
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`} to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
replace replace
/> />
); );

View File

@@ -27,11 +27,8 @@ export const TotpPage = () => {
const redirectTimer = useRef<number | null>(null); const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const { const redirectUri = searchParams.get("redirect_uri") || undefined;
values: props, const oidcParams = useOIDCParams(searchParams);
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);
const totpMutation = useMutation({ const totpMutation = useMutation({
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values), mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
@@ -42,13 +39,13 @@ export const TotpPage = () => {
}); });
redirectTimer.current = window.setTimeout(() => { redirectTimer.current = window.setTimeout(() => {
if (isOidc) { if (oidcParams.isOidc) {
window.location.replace(`/authorize?${compiledOIDCParams}`); window.location.replace(`/authorize?${oidcParams.compiled}`);
return; return;
} }
window.location.replace( window.location.replace(
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, `/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
); );
}, 500); }, 500);
}, },

View File

@@ -1,9 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
// Resolve paths // Resolve paths
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
}, },
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
@@ -26,7 +25,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
}, },
"include": ["src"] "include": ["src"],
} }

View File

@@ -2,12 +2,11 @@
"files": [], "files": [],
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" } { "path": "./tsconfig.node.json" },
], ],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
} },
} },
} }

70
go.mod
View File

@@ -1,62 +1,67 @@
module github.com/steveiliop56/tinyauth module github.com/steveiliop56/tinyauth
go 1.25.0 go 1.26.0
replace github.com/traefik/paerser v0.2.2 => ./paerser
require ( require (
charm.land/huh/v2 v2.0.3
github.com/cenkalti/backoff/v5 v5.0.3 github.com/cenkalti/backoff/v5 v5.0.3
github.com/charmbracelet/huh v0.8.0
github.com/docker/docker v28.5.2+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/go-jose/go-jose/v4 v4.1.3 github.com/go-jose/go-jose/v4 v4.1.4
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.13
github.com/golang-migrate/migrate/v4 v4.19.1 github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/go-querystring v1.2.0 github.com/google/go-querystring v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mdp/qrterminal/v3 v3.2.1 github.com/mdp/qrterminal/v3 v3.2.1
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.35.0
github.com/traefik/paerser v0.2.2 github.com/stretchr/testify v1.11.1
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
github.com/weppos/publicsuffix-go v0.50.3 github.com/weppos/publicsuffix-go v0.50.3
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.49.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
gotest.tools/v3 v3.5.2 gotest.tools/v3 v3.5.2
modernc.org/sqlite v1.47.0 modernc.org/sqlite v1.48.0
) )
require ( require (
charm.land/bubbles/v2 v2.0.0 // indirect
charm.land/bubbletea/v2 v2.0.2 // indirect
charm.land/lipgloss/v2 v2.0.1 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect github.com/boombuler/barcode v1.0.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
@@ -70,15 +75,13 @@ require (
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
@@ -88,14 +91,13 @@ require (
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -106,19 +108,21 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.70.0 // indirect modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect

222
go.sum
View File

@@ -1,28 +1,35 @@
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
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.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 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -34,38 +41,38 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
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=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -74,7 +81,6 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -91,8 +97,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -105,8 +109,8 @@ github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -126,7 +130,6 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -137,20 +140,16 @@ github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJ
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -175,29 +174,22 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -215,12 +207,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -242,35 +230,31 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
@@ -279,101 +263,67 @@ github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zu
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4= github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -401,8 +351,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -0,0 +1 @@
ALTER TABLE "oidc_codes" DROP COLUMN "code_challenge";

View File

@@ -0,0 +1 @@
ALTER TABLE "oidc_codes" ADD COLUMN "code_challenge" TEXT DEFAULT "";

View File

@@ -2,152 +2,133 @@ package controller_test
import ( import (
"encoding/json" "encoding/json"
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
) )
var contextControllerCfg = controller.ContextControllerConfig{ func TestContextController(t *testing.T) {
Providers: []controller.Provider{ tlog.NewTestLogger().Init()
controllerConfig := controller.ContextControllerConfig{
Providers: []controller.Provider{
{
Name: "Local",
ID: "local",
OAuth: false,
},
},
Title: "Tinyauth",
AppURL: "https://tinyauth.example.com",
CookieDomain: "example.com",
ForgotPasswordMessage: "foo",
BackgroundImage: "/background.jpg",
OAuthAutoRedirect: "none",
WarningsEnabled: true,
}
tests := []struct {
description string
middlewares []gin.HandlerFunc
expected string
path string
}{
{ {
Name: "Local", description: "Ensure context controller returns app context",
ID: "local", middlewares: []gin.HandlerFunc{},
OAuth: false, path: "/api/context/app",
expected: func() string {
expectedAppContextResponse := controller.AppContextResponse{
Status: 200,
Message: "Success",
Providers: controllerConfig.Providers,
Title: controllerConfig.Title,
AppURL: controllerConfig.AppURL,
CookieDomain: controllerConfig.CookieDomain,
ForgotPasswordMessage: controllerConfig.ForgotPasswordMessage,
BackgroundImage: controllerConfig.BackgroundImage,
OAuthAutoRedirect: controllerConfig.OAuthAutoRedirect,
WarningsEnabled: controllerConfig.WarningsEnabled,
}
bytes, err := json.Marshal(expectedAppContextResponse)
assert.NoError(t, err)
return string(bytes)
}(),
}, },
{ {
Name: "Google", description: "Ensure user context returns 401 when unauthorized",
ID: "google", middlewares: []gin.HandlerFunc{},
OAuth: true, path: "/api/context/user",
expected: func() string {
expectedUserContextResponse := controller.UserContextResponse{
Status: 401,
Message: "Unauthorized",
}
bytes, err := json.Marshal(expectedUserContextResponse)
assert.NoError(t, err)
return string(bytes)
}(),
}, },
}, {
Title: "Test App", description: "Ensure user context returns when authorized",
AppURL: "http://localhost:8080", middlewares: []gin.HandlerFunc{
CookieDomain: "localhost", func(c *gin.Context) {
ForgotPasswordMessage: "Contact admin to reset your password.", c.Set("context", &config.UserContext{
BackgroundImage: "/assets/bg.jpg", Username: "johndoe",
OAuthAutoRedirect: "google", Name: "John Doe",
WarningsEnabled: true, Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
} Provider: "local",
IsLoggedIn: true,
var contextCtrlTestContext = config.UserContext{ })
Username: "testuser", },
Name: "testuser", },
Email: "test@example.com", path: "/api/context/user",
IsLoggedIn: true, expected: func() string {
IsBasicAuth: false, expectedUserContextResponse := controller.UserContextResponse{
OAuth: false, Status: 200,
Provider: "local", Message: "Success",
TotpPending: false, IsLoggedIn: true,
OAuthGroups: "", Username: "johndoe",
TotpEnabled: false, Name: "John Doe",
OAuthSub: "", Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
} Provider: "local",
}
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { bytes, err := json.Marshal(expectedUserContextResponse)
tlog.NewSimpleLogger().Init() assert.NoError(t, err)
return string(bytes)
// Setup }(),
gin.SetMode(gin.TestMode)
router := gin.Default()
recorder := httptest.NewRecorder()
if middlewares != nil {
for _, m := range *middlewares {
router.Use(m)
}
}
group := router.Group("/api")
ctrl := controller.NewContextController(contextControllerCfg, group)
ctrl.SetupRoutes()
return router, recorder
}
func TestAppContextHandler(t *testing.T) {
expectedRes := controller.AppContextResponse{
Status: 200,
Message: "Success",
Providers: contextControllerCfg.Providers,
Title: contextControllerCfg.Title,
AppURL: contextControllerCfg.AppURL,
CookieDomain: contextControllerCfg.CookieDomain,
ForgotPasswordMessage: contextControllerCfg.ForgotPasswordMessage,
BackgroundImage: contextControllerCfg.BackgroundImage,
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
WarningsEnabled: contextControllerCfg.WarningsEnabled,
}
router, recorder := setupContextController(nil)
req := httptest.NewRequest("GET", "/api/context/app", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
var ctrlRes controller.AppContextResponse
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
assert.NilError(t, err)
assert.DeepEqual(t, expectedRes, ctrlRes)
}
func TestUserContextHandler(t *testing.T) {
expectedRes := controller.UserContextResponse{
Status: 200,
Message: "Success",
IsLoggedIn: contextCtrlTestContext.IsLoggedIn,
Username: contextCtrlTestContext.Username,
Name: contextCtrlTestContext.Name,
Email: contextCtrlTestContext.Email,
Provider: contextCtrlTestContext.Provider,
OAuth: contextCtrlTestContext.OAuth,
TotpPending: contextCtrlTestContext.TotpPending,
OAuthName: contextCtrlTestContext.OAuthName,
}
// Test with context
router, recorder := setupContextController(&[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &contextCtrlTestContext)
c.Next()
}, },
})
req := httptest.NewRequest("GET", "/api/context/user", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
var ctrlRes controller.UserContextResponse
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
assert.NilError(t, err)
assert.DeepEqual(t, expectedRes, ctrlRes)
// Test no context
expectedRes = controller.UserContextResponse{
Status: 401,
Message: "Unauthorized",
IsLoggedIn: false,
} }
router, recorder = setupContextController(nil) for _, test := range tests {
req = httptest.NewRequest("GET", "/api/context/user", nil) t.Run(test.description, func(t *testing.T) {
router.ServeHTTP(recorder, req) router := gin.Default()
assert.Equal(t, 200, recorder.Code) for _, middleware := range test.middlewares {
router.Use(middleware)
}
err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes) group := router.Group("/api")
gin.SetMode(gin.TestMode)
assert.NilError(t, err) contextController := controller.NewContextController(controllerConfig, group)
assert.DeepEqual(t, expectedRes, ctrlRes) contextController.SetupRoutes()
recorder := httptest.NewRecorder()
request, err := http.NewRequest("GET", test.path, nil)
assert.NoError(t, err)
router.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, test.expected, recorder.Body.String())
})
}
} }

View File

@@ -19,7 +19,7 @@ func (controller *HealthController) SetupRoutes() {
func (controller *HealthController) healthHandler(c *gin.Context) { func (controller *HealthController) healthHandler(c *gin.Context) {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": "ok", "status": 200,
"message": "Healthy", "message": "Healthy",
}) })
} }

View File

@@ -0,0 +1,73 @@
package controller_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
)
func TestHealthController(t *testing.T) {
tlog.NewTestLogger().Init()
tests := []struct {
description string
path string
method string
expected string
}{
{
description: "Ensure health endpoint returns 200 OK",
path: "/api/healthz",
method: "GET",
expected: func() string {
expectedHealthResponse := map[string]any{
"status": 200,
"message": "Healthy",
}
bytes, err := json.Marshal(expectedHealthResponse)
assert.NoError(t, err)
return string(bytes)
}(),
},
{
description: "Ensure health endpoint returns 200 OK for HEAD request",
path: "/api/healthz",
method: "HEAD",
expected: func() string {
expectedHealthResponse := map[string]any{
"status": 200,
"message": "Healthy",
}
bytes, err := json.Marshal(expectedHealthResponse)
assert.NoError(t, err)
return string(bytes)
}(),
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
router := gin.Default()
group := router.Group("/api")
gin.SetMode(gin.TestMode)
healthController := controller.NewHealthController(group)
healthController.SetupRoutes()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(test.method, test.path, nil)
assert.NoError(t, err)
router.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, test.expected, recorder.Body.String())
})
}
}

View File

@@ -62,7 +62,29 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return return
} }
sessionId, session, err := controller.auth.NewOAuthSession(req.Provider) var reqParams service.OAuthURLParams
err = c.BindQuery(&reqParams)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to bind query parameters")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return
}
if !controller.isOidcRequest(reqParams) {
isRedirectSafe := utils.IsRedirectSafe(reqParams.RedirectURI, controller.config.CookieDomain)
if !isRedirectSafe {
tlog.App.Warn().Str("redirect_uri", reqParams.RedirectURI).Msg("Unsafe redirect URI detected, ignoring")
reqParams.RedirectURI = ""
}
}
sessionId, _, err := controller.auth.NewOAuthSession(req.Provider, reqParams)
if err != nil { if err != nil {
tlog.App.Error().Err(err).Msg("Failed to create OAuth session") tlog.App.Error().Err(err).Msg("Failed to create OAuth session")
@@ -85,20 +107,6 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
} }
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
c.SetCookie(controller.config.CSRFCookieName, session.State, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
redirectURI := c.Query("redirect_uri")
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
if !isRedirectSafe {
tlog.App.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
redirectURI = ""
}
if redirectURI != "" && isRedirectSafe {
tlog.App.Debug().Msg("Setting redirect URI cookie")
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
}
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -129,19 +137,23 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
} }
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
defer controller.auth.EndOAuthSession(sessionIdCookie)
state := c.Query("state") oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
if err != nil || state != csrfCookie { if err != nil {
tlog.App.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") tlog.App.Error().Err(err).Msg("Failed to get OAuth pending session")
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return return
} }
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) defer controller.auth.EndOAuthSession(sessionIdCookie)
state := c.Query("state")
if state != oauthPendingSession.State {
tlog.App.Warn().Err(err).Msg("CSRF token mismatch")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
code := c.Query("code") code := c.Query("code")
_, err = controller.auth.GetOAuthToken(sessionIdCookie, code) _, err = controller.auth.GetOAuthToken(sessionIdCookie, code)
@@ -198,7 +210,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
username = strings.Replace(user.Email, "@", "_", 1) username = strings.Replace(user.Email, "@", "_", 1)
} }
service, err := controller.auth.GetOAuthService(sessionIdCookie) svc, err := controller.auth.GetOAuthService(sessionIdCookie)
if err != nil { if err != nil {
tlog.App.Error().Err(err).Msg("Failed to get OAuth service for session") tlog.App.Error().Err(err).Msg("Failed to get OAuth service for session")
@@ -206,13 +218,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
return return
} }
if svc.ID() != req.Provider {
tlog.App.Error().Msgf("OAuth service ID mismatch: expected %s, got %s", svc.ID(), req.Provider)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
sessionCookie := repository.Session{ sessionCookie := repository.Session{
Username: username, Username: username,
Name: name, Name: name,
Email: user.Email, Email: user.Email,
Provider: req.Provider, Provider: svc.ID(),
OAuthGroups: utils.CoalesceToString(user.Groups), OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: service.Name(), OAuthName: svc.Name(),
OAuthSub: user.Sub, OAuthSub: user.Sub,
} }
@@ -228,24 +246,39 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider) tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
redirectURI, err := c.Cookie(controller.config.RedirectCookieName) if controller.isOidcRequest(oauthPendingSession.CallbackParams) {
tlog.App.Debug().Msg("OIDC request, redirecting to authorize page")
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) { queries, err := query.Values(oauthPendingSession.CallbackParams)
tlog.App.Debug().Msg("No redirect URI cookie found, redirecting to app root") if err != nil {
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL) tlog.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/authorize?%s", controller.config.AppURL, queries.Encode()))
return return
} }
queries, err := query.Values(config.RedirectQuery{ if oauthPendingSession.CallbackParams.RedirectURI != "" {
RedirectURI: redirectURI, queries, err := query.Values(config.RedirectQuery{
}) RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
})
if err != nil { if err != nil {
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query") tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode()))
return return
} }
c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode())) }
func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams) bool {
return params.Scope != "" &&
params.ResponseType != "" &&
params.ClientID != "" &&
params.RedirectURI != ""
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils" "github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
@@ -34,6 +35,7 @@ type TokenRequest struct {
RefreshToken string `form:"refresh_token" url:"refresh_token"` RefreshToken string `form:"refresh_token" url:"refresh_token"`
ClientSecret string `form:"client_secret" url:"client_secret"` ClientSecret string `form:"client_secret" url:"client_secret"`
ClientID string `form:"client_id" url:"client_id"` ClientID string `form:"client_id" url:"client_id"`
CodeVerifier string `form:"code_verifier" url:"code_verifier"`
} }
type CallbackError struct { type CallbackError struct {
@@ -69,6 +71,7 @@ func (controller *OIDCController) SetupRoutes() {
oidcGroup.POST("/authorize", controller.Authorize) oidcGroup.POST("/authorize", controller.Authorize)
oidcGroup.POST("/token", controller.Token) oidcGroup.POST("/token", controller.Token)
oidcGroup.GET("/userinfo", controller.Userinfo) oidcGroup.GET("/userinfo", controller.Userinfo)
oidcGroup.POST("/userinfo", controller.Userinfo)
} }
func (controller *OIDCController) GetClientInfo(c *gin.Context) { func (controller *OIDCController) GetClientInfo(c *gin.Context) {
@@ -235,7 +238,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
if !ok { if !ok {
tlog.App.Error().Msg("Missing authorization header") tlog.App.Error().Msg("Missing authorization header")
c.Header("www-authenticate", "basic") c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid_client", "error": "invalid_client",
}) })
@@ -308,6 +311,16 @@ func (controller *OIDCController) Token(c *gin.Context) {
return return
} }
ok := controller.oidc.ValidatePKCE(entry.CodeChallenge, req.CodeVerifier)
if !ok {
tlog.App.Warn().Msg("PKCE validation failed")
c.JSON(400, gin.H{
"error": "invalid_grant",
})
return
}
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry) tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
if err != nil { if err != nil {
@@ -364,22 +377,48 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
return return
} }
var token string
authorization := c.GetHeader("Authorization") authorization := c.GetHeader("Authorization")
if authorization != "" {
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
if !ok {
tlog.App.Warn().Msg("OIDC userinfo accessed with malformed authorization header")
c.JSON(401, gin.H{
"error": "invalid_request",
})
return
}
tokenType, token, ok := strings.Cut(authorization, " ") if strings.ToLower(tokenType) != "bearer" {
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
c.JSON(401, gin.H{
"error": "invalid_request",
})
return
}
if !ok { token = bearerToken
} else if c.Request.Method == http.MethodPost {
if c.ContentType() != "application/x-www-form-urlencoded" {
tlog.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
c.JSON(400, gin.H{
"error": "invalid_request",
})
return
}
token = c.PostForm("access_token")
if token == "" {
tlog.App.Warn().Msg("OIDC userinfo POST accessed without access_token in body")
c.JSON(401, gin.H{
"error": "invalid_request",
})
return
}
} else {
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header") tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"error": "invalid_grant", "error": "invalid_request",
})
return
}
if strings.ToLower(tokenType) != "bearer" {
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
c.JSON(401, gin.H{
"error": "invalid_grant",
}) })
return return
} }

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,15 @@ const (
ForwardAuth ForwardAuth
) )
type ProxyType int
const (
Traefik ProxyType = iota
Caddy
Envoy
Nginx
)
var BrowserUserAgentRegex = regexp.MustCompile("Chrome|Gecko|AppleWebKit|Opera|Edge") var BrowserUserAgentRegex = regexp.MustCompile("Chrome|Gecko|AppleWebKit|Opera|Edge")
type Proxy struct { type Proxy struct {
@@ -38,6 +47,7 @@ type ProxyContext struct {
Method string Method string
Type AuthModuleType Type AuthModuleType
IsBrowser bool IsBrowser bool
ProxyType ProxyType
} }
type ProxyControllerConfig struct { type ProxyControllerConfig struct {
@@ -121,7 +131,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
} }
if !controller.auth.CheckIP(acls.IP, clientIP) { if !controller.auth.CheckIP(acls.IP, clientIP) {
if !controller.useFriendlyError(proxyCtx) { if !controller.useBrowserResponse(proxyCtx) {
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -165,7 +175,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
if !userAllowed { if !userAllowed {
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource") tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
if !controller.useFriendlyError(proxyCtx) { if !controller.useBrowserResponse(proxyCtx) {
c.JSON(403, gin.H{ c.JSON(403, gin.H{
"status": 403, "status": 403,
"message": "Forbidden", "message": "Forbidden",
@@ -205,7 +215,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
if !groupOK { if !groupOK {
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements") tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
if !controller.useFriendlyError(proxyCtx) { if !controller.useBrowserResponse(proxyCtx) {
c.JSON(403, gin.H{ c.JSON(403, gin.H{
"status": 403, "status": 403,
"message": "Forbidden", "message": "Forbidden",
@@ -256,7 +266,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return return
} }
if !controller.useFriendlyError(proxyCtx) { if !controller.useBrowserResponse(proxyCtx) {
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -296,7 +306,7 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
} }
func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyContext) { func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyContext) {
if !controller.useFriendlyError(proxyCtx) { if !controller.useBrowserResponse(proxyCtx) {
c.JSON(500, gin.H{ c.JSON(500, gin.H{
"status": 500, "status": 500,
"message": "Internal Server Error", "message": "Internal Server Error",
@@ -312,8 +322,34 @@ func (controller *ProxyController) getHeader(c *gin.Context, header string) (str
return val, strings.TrimSpace(val) != "" return val, strings.TrimSpace(val) != ""
} }
func (controller *ProxyController) useFriendlyError(proxyCtx ProxyContext) bool { func (controller *ProxyController) useBrowserResponse(proxyCtx ProxyContext) bool {
return (proxyCtx.Type == ForwardAuth || proxyCtx.Type == ExtAuthz) && proxyCtx.IsBrowser // If it's nginx or envoy we need non-browser response
if proxyCtx.ProxyType == Nginx || proxyCtx.ProxyType == Envoy {
return false
}
// For other proxies (traefik or caddy) we can check
// the user agent to determine if it's a browser or not
if proxyCtx.IsBrowser {
return true
}
return false
}
func (controller *ProxyController) getProxyType(proxy string) (ProxyType, error) {
switch proxy {
case "traefik":
return Traefik, nil
case "caddy":
return Caddy, nil
case "envoy":
return Envoy, nil
case "nginx":
return Nginx, nil
default:
return 0, fmt.Errorf("unsupported proxy type: %v", proxy)
}
} }
// Code below is inspired from https://github.com/authelia/authelia/blob/master/internal/handlers/handler_authz.go // Code below is inspired from https://github.com/authelia/authelia/blob/master/internal/handlers/handler_authz.go
@@ -417,13 +453,13 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont
}, nil }, nil
} }
func (controller *ProxyController) determineAuthModules(proxy string) []AuthModuleType { func (controller *ProxyController) determineAuthModules(proxy ProxyType) []AuthModuleType {
switch proxy { switch proxy {
case "traefik", "caddy": case Traefik, Caddy:
return []AuthModuleType{ForwardAuth} return []AuthModuleType{ForwardAuth}
case "envoy": case Envoy:
return []AuthModuleType{ExtAuthz, ForwardAuth} return []AuthModuleType{ExtAuthz, ForwardAuth}
case "nginx": case Nginx:
return []AuthModuleType{AuthRequest, ForwardAuth} return []AuthModuleType{AuthRequest, ForwardAuth}
default: default:
return []AuthModuleType{} return []AuthModuleType{}
@@ -462,9 +498,15 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
return ProxyContext{}, err return ProxyContext{}, err
} }
proxy, err := controller.getProxyType(req.Proxy)
if err != nil {
return ProxyContext{}, err
}
tlog.App.Debug().Msgf("Proxy: %v", req.Proxy) tlog.App.Debug().Msgf("Proxy: %v", req.Proxy)
authModules := controller.determineAuthModules(req.Proxy) authModules := controller.determineAuthModules(proxy)
if len(authModules) == 0 { if len(authModules) == 0 {
return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy) return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy)
@@ -497,5 +539,6 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
} }
ctx.IsBrowser = isBrowser ctx.IsBrowser = isBrowser
ctx.ProxyType = proxy
return ctx, nil return ctx, nil
} }

View File

@@ -1,302 +1,445 @@
package controller_test package controller_test
import ( import (
"net/http"
"net/http/httptest" "net/http/httptest"
"path"
"testing" "testing"
"github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin" "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert" "github.com/stretchr/testify/require"
) )
var loggedInCtx = config.UserContext{ func TestProxyController(t *testing.T) {
Username: "test", tlog.NewTestLogger().Init()
Name: "Test", tempDir := t.TempDir()
Email: "test@example.com",
IsLoggedIn: true,
Provider: "local",
}
func setupProxyController(t *testing.T, middlewares []gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { authServiceCfg := service.AuthServiceConfig{
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()
if len(middlewares) > 0 {
for _, m := range middlewares {
router.Use(m)
}
}
group := router.Group("/api")
recorder := httptest.NewRecorder()
// Mock app
app := bootstrap.NewBootstrapApp(config.Config{})
// Database
db, err := app.SetupDatabase(":memory:")
assert.NilError(t, err)
// Queries
queries := repository.New(db)
// Docker
dockerService := service.NewDockerService()
assert.NilError(t, dockerService.Init())
// Access controls
accessControlsService := service.NewAccessControlsService(dockerService, map[string]config.App{
"whoami": {
Path: config.AppPath{
Allow: "/allow",
},
},
})
assert.NilError(t, accessControlsService.Init())
// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{ Users: []config.User{
{ {
Username: "testuser", Username: "testuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
}, },
{ {
Username: "totpuser", Username: "totpuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
TotpSecret: "foo", TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
}, },
}, },
OauthWhitelist: []string{}, SessionExpiry: 10, // 10 seconds, useful for testing
SessionExpiry: 3600, CookieDomain: "example.com",
SessionMaxLifetime: 0, LoginTimeout: 10, // 10 seconds, useful for testing
SecureCookie: false, LoginMaxRetries: 3,
CookieDomain: "localhost", SessionCookieName: "tinyauth-session",
LoginTimeout: 300, }
LoginMaxRetries: 3,
SessionCookieName: "tinyauth-session",
}, dockerService, nil, queries, &service.OAuthBrokerService{})
// Controller controllerCfg := controller.ProxyControllerConfig{
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{ AppURL: "https://tinyauth.example.com",
AppURL: "http://tinyauth.example.com", }
}, group, accessControlsService, authService)
ctrl.SetupRoutes()
return router, recorder acls := map[string]config.App{
} "app_path_allow": {
Config: config.AppConfig{
// TODO: Needs tests for context middleware Domain: "path-allow.example.com",
},
func TestProxyHandler(t *testing.T) { Path: config.AppPath{
// Test logged out user traefik/caddy (forward_auth) Allow: "/allowed",
router, recorder := setupProxyController(t, nil) },
},
req, err := http.NewRequest("GET", "/api/auth/traefik", nil) "app_user_allow": {
assert.NilError(t, err) Config: config.AppConfig{
Domain: "user-allow.example.com",
req.Header.Set("x-forwarded-host", "whoami.example.com") },
req.Header.Set("x-forwarded-proto", "http") Users: config.AppUsers{
req.Header.Set("x-forwarded-uri", "/") Allow: "testuser",
},
router.ServeHTTP(recorder, req) },
assert.Equal(t, recorder.Code, http.StatusUnauthorized) "ip_bypass": {
Config: config.AppConfig{
// Test logged out user nginx (auth_request) Domain: "ip-bypass.example.com",
router, recorder = setupProxyController(t, nil) },
IP: config.AppIP{
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) Bypass: []string{"10.10.10.10"},
assert.NilError(t, err) },
},
req.Header.Set("x-original-url", "http://whoami.example.com/") }
router.ServeHTTP(recorder, req) const browserUserAgent = `
assert.Equal(t, recorder.Code, http.StatusUnauthorized) Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
// Test logged out user envoy (ext_authz) simpleCtx := func(c *gin.Context) {
router, recorder = setupProxyController(t, nil) c.Set("context", &config.UserContext{
Username: "testuser",
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil) Name: "Testuser",
assert.NilError(t, err) Email: "testuser@example.com",
IsLoggedIn: true,
req.Host = "whoami.example.com" Provider: "local",
req.Header.Set("x-forwarded-proto", "http") })
c.Next()
router.ServeHTTP(recorder, req) }
assert.Equal(t, recorder.Code, http.StatusUnauthorized)
simpleCtxTotp := func(c *gin.Context) {
// Test logged in user traefik/caddy (forward_auth) c.Set("context", &config.UserContext{
router, recorder = setupProxyController(t, []gin.HandlerFunc{ Username: "totpuser",
func(c *gin.Context) { Name: "Totpuser",
c.Set("context", &loggedInCtx) Email: "totpuser@example.com",
c.Next() IsLoggedIn: true,
}, Provider: "local",
}) TotpEnabled: true,
})
req, err = http.NewRequest("GET", "/api/auth/traefik", nil) c.Next()
assert.NilError(t, err) }
req.Header.Set("x-forwarded-host", "whoami.example.com") type testCase struct {
req.Header.Set("x-forwarded-proto", "http") description string
req.Header.Set("x-forwarded-uri", "/") middlewares []gin.HandlerFunc
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
router.ServeHTTP(recorder, req) }
assert.Equal(t, recorder.Code, http.StatusOK)
tests := []testCase{
// Test logged in user nginx (auth_request) {
router, recorder = setupProxyController(t, []gin.HandlerFunc{ description: "Default forward auth should be detected and used",
func(c *gin.Context) { middlewares: []gin.HandlerFunc{},
c.Set("context", &loggedInCtx) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
c.Next() req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
}, req.Header.Set("x-forwarded-host", "test.example.com")
}) req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("user-agent", browserUserAgent)
assert.NilError(t, err) router.ServeHTTP(recorder, req)
req.Header.Set("x-original-url", "http://whoami.example.com/") assert.Equal(t, 307, recorder.Code)
location := recorder.Header().Get("Location")
router.ServeHTTP(recorder, req) assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=")
assert.Equal(t, recorder.Code, http.StatusOK) assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F")
},
// Test logged in user envoy (ext_authz) },
router, recorder = setupProxyController(t, []gin.HandlerFunc{ {
func(c *gin.Context) { description: "Auth request (nginx) should be detected and used",
c.Set("context", &loggedInCtx) middlewares: []gin.HandlerFunc{},
c.Next() run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
}, req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
}) req.Header.Set("x-original-url", "https://test.example.com/")
router.ServeHTTP(recorder, req)
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil) assert.Equal(t, 401, recorder.Code)
assert.NilError(t, err) },
},
req.Host = "whoami.example.com" {
req.Header.Set("x-forwarded-proto", "http") description: "Ext authz (envoy) should be detected and used",
middlewares: []gin.HandlerFunc{},
router.ServeHTTP(recorder, req) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
assert.Equal(t, recorder.Code, http.StatusOK) req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) // test a different method for envoy
req.Host = "test.example.com"
// Test ACL allow caddy/traefik (forward_auth) req.Header.Set("x-forwarded-proto", "https")
router, recorder = setupProxyController(t, nil) router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
req, err = http.NewRequest("GET", "/api/auth/traefik", nil) },
assert.NilError(t, err) },
{
req.Header.Set("x-forwarded-host", "whoami.example.com") description: "Ensure forward auth fallback for nginx",
req.Header.Set("x-forwarded-proto", "http") middlewares: []gin.HandlerFunc{},
req.Header.Set("x-forwarded-uri", "/allow") run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
router.ServeHTTP(recorder, req) req.Header.Set("x-forwarded-host", "test.example.com")
assert.Equal(t, recorder.Code, http.StatusOK) req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
// Test ACL allow nginx router.ServeHTTP(recorder, req)
router, recorder = setupProxyController(t, nil) assert.Equal(t, 401, recorder.Code)
},
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) },
assert.NilError(t, err) {
description: "Ensure forward auth fallback for envoy",
req.Header.Set("x-original-url", "http://whoami.example.com/allow") middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
router.ServeHTTP(recorder, req) req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
assert.Equal(t, recorder.Code, http.StatusOK) req.Header.Set("x-forwarded-host", "test.example.com")
req.Header.Set("x-forwarded-proto", "https")
// Test ACL allow envoy req.Header.Set("x-forwarded-uri", "/hello")
router, recorder = setupProxyController(t, nil) router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/allow", nil) },
assert.NilError(t, err) },
{
req.Host = "whoami.example.com" description: "Ensure forward auth fallback for nginx with browser user agent",
req.Header.Set("x-forwarded-proto", "http") middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
router.ServeHTTP(recorder, req) req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
assert.Equal(t, recorder.Code, http.StatusOK) req.Header.Set("x-forwarded-host", "test.example.com")
req.Header.Set("x-forwarded-proto", "https")
// Test traefik/caddy (forward_auth) without required headers req.Header.Set("x-forwarded-uri", "/")
router, recorder = setupProxyController(t, nil) req.Header.Set("user-agent", browserUserAgent)
router.ServeHTTP(recorder, req)
req, err = http.NewRequest("GET", "/api/auth/traefik", nil) assert.Equal(t, 401, recorder.Code)
assert.NilError(t, err) },
},
router.ServeHTTP(recorder, req) {
assert.Equal(t, recorder.Code, http.StatusBadRequest) description: "Ensure forward auth fallback for envoy with browser user agent",
middlewares: []gin.HandlerFunc{},
// Test nginx (forward_auth) without required headers run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
router, recorder = setupProxyController(t, nil) req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
req.Header.Set("x-forwarded-host", "test.example.com")
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-forwarded-proto", "https")
assert.NilError(t, err) req.Header.Set("x-forwarded-uri", "/hello")
req.Header.Set("user-agent", browserUserAgent)
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
assert.Equal(t, recorder.Code, http.StatusBadRequest) assert.Equal(t, 401, recorder.Code)
},
// Test envoy (forward_auth) without required headers },
router, recorder = setupProxyController(t, nil) {
description: "Ensure forward auth with is browser false returns json",
req, err = http.NewRequest("GET", "/api/auth/envoy", nil) middlewares: []gin.HandlerFunc{},
assert.NilError(t, err) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
router.ServeHTTP(recorder, req) req.Header.Set("x-forwarded-host", "test.example.com")
assert.Equal(t, recorder.Code, http.StatusBadRequest) req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
// Test nginx (auth_request) with forward_auth fallback with ACLs router.ServeHTTP(recorder, req)
router, recorder = setupProxyController(t, nil)
assert.Equal(t, 401, recorder.Code)
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) assert.Contains(t, recorder.Body.String(), `"status":401`)
assert.NilError(t, err) assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
},
req.Header.Set("x-forwarded-host", "whoami.example.com") },
req.Header.Set("x-forwarded-proto", "http") {
req.Header.Set("x-forwarded-uri", "/allow") description: "Ensure forward auth with caddy and browser user agent returns redirect",
middlewares: []gin.HandlerFunc{},
router.ServeHTTP(recorder, req) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
assert.Equal(t, recorder.Code, http.StatusOK) req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", "test.example.com")
// Test envoy (ext_authz) with forward_auth fallback with ACLs req.Header.Set("x-forwarded-proto", "https")
router, recorder = setupProxyController(t, nil) req.Header.Set("x-forwarded-uri", "/")
req.Header.Set("user-agent", browserUserAgent)
req, err = http.NewRequest("GET", "/api/auth/envoy", nil) router.ServeHTTP(recorder, req)
assert.NilError(t, err)
assert.Equal(t, 307, recorder.Code)
req.Header.Set("x-forwarded-host", "whoami.example.com") location := recorder.Header().Get("Location")
req.Header.Set("x-forwarded-proto", "http") assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=")
req.Header.Set("x-forwarded-uri", "/allow") assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F")
},
router.ServeHTTP(recorder, req) },
assert.Equal(t, recorder.Code, http.StatusOK) {
description: "Ensure forward auth with caddy and non browser user agent returns json",
// Test envoy (ext_authz) with empty path middlewares: []gin.HandlerFunc{},
router, recorder = setupProxyController(t, nil) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req, err = http.NewRequest("GET", "/api/auth/envoy", nil) req.Header.Set("x-forwarded-host", "test.example.com")
assert.NilError(t, err) req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
req.Host = "whoami.example.com" router.ServeHTTP(recorder, req)
req.Header.Set("x-forwarded-proto", "http")
assert.Equal(t, 401, recorder.Code)
router.ServeHTTP(recorder, req) assert.Contains(t, recorder.Body.String(), `"status":401`)
assert.Equal(t, recorder.Code, http.StatusUnauthorized) assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
},
// Ensure forward_auth fallback works with path (should ignore) },
router, recorder = setupProxyController(t, nil) {
description: "Ensure normal authentication flow for forward auth",
req, err = http.NewRequest("GET", "/api/auth/traefik?path=/allow", nil) middlewares: []gin.HandlerFunc{
assert.NilError(t, err) simpleCtx,
},
req.Header.Set("x-forwarded-proto", "http") run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req.Header.Set("x-forwarded-host", "whoami.example.com") req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-uri", "/allow") req.Header.Set("x-forwarded-host", "test.example.com")
req.Header.Set("x-forwarded-proto", "https")
router.ServeHTTP(recorder, req) req.Header.Set("x-forwarded-uri", "/")
assert.Equal(t, recorder.Code, http.StatusOK) router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
},
},
{
description: "Ensure normal authentication flow for nginx auth request",
middlewares: []gin.HandlerFunc{
simpleCtx,
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
req.Header.Set("x-original-url", "https://test.example.com/")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
},
},
{
description: "Ensure normal authentication flow for envoy ext authz",
middlewares: []gin.HandlerFunc{
simpleCtx,
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
req.Host = "test.example.com"
req.Header.Set("x-forwarded-proto", "https")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
},
},
{
description: "Ensure path allow ACL works on forward auth",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", "path-allow.example.com")
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/allowed")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure path allow ACL works on nginx auth request",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
req.Header.Set("x-original-url", "https://path-allow.example.com/allowed")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure path allow ACL works on envoy ext authz",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/allowed", nil)
req.Host = "path-allow.example.com"
req.Header.Set("x-forwarded-proto", "https")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure ip bypass ACL works on forward auth",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", "ip-bypass.example.com")
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
req.Header.Set("x-forwarded-for", "10.10.10.10")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure ip bypass ACL works on nginx auth request",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
req.Header.Set("x-original-url", "https://ip-bypass.example.com/")
req.Header.Set("x-forwarded-for", "10.10.10.10")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure ip bypass ACL works on envoy ext authz",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
req.Host = "ip-bypass.example.com"
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-for", "10.10.10.10")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure user allow ACL allows correct user (should allow testuser)",
middlewares: []gin.HandlerFunc{
simpleCtx,
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", "user-allow.example.com")
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
},
},
{
description: "Ensure user allow ACL blocks incorrect user (should block totpuser)",
middlewares: []gin.HandlerFunc{
simpleCtxTotp,
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", "user-allow.example.com")
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
router.ServeHTTP(recorder, req)
assert.Equal(t, 403, recorder.Code)
assert.Equal(t, "", recorder.Header().Get("remote-user"))
assert.Equal(t, "", recorder.Header().Get("remote-name"))
assert.Equal(t, "", recorder.Header().Get("remote-email"))
},
},
}
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err)
queries := repository.New(db)
docker := service.NewDockerService()
err = docker.Init()
require.NoError(t, err)
ldap := service.NewLdapService(service.LdapServiceConfig{})
err = ldap.Init()
require.NoError(t, err)
broker := service.NewOAuthBrokerService(oauthBrokerCfgs)
err = broker.Init()
require.NoError(t, err)
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
err = authService.Init()
require.NoError(t, err)
aclsService := service.NewAccessControlsService(docker, acls)
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
router := gin.Default()
for _, m := range test.middlewares {
router.Use(m)
}
group := router.Group("/api")
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
proxyController := controller.NewProxyController(controllerCfg, group, aclsService, authService)
proxyController.SetupRoutes()
test.run(t, router, recorder)
})
}
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
})
} }

View File

@@ -3,57 +3,83 @@ package controller_test
import ( import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path"
"testing" "testing"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gotest.tools/v3/assert" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestResourcesHandler(t *testing.T) { func TestResourcesController(t *testing.T) {
// Setup tlog.NewTestLogger().Init()
gin.SetMode(gin.TestMode) tempDir := t.TempDir()
router := gin.New()
group := router.Group("/")
ctrl := controller.NewResourcesController(controller.ResourcesControllerConfig{ resourcesControllerCfg := controller.ResourcesControllerConfig{
Path: "/tmp/tinyauth", Path: path.Join(tempDir, "resources"),
Enabled: true, Enabled: true,
}, group) }
ctrl.SetupRoutes()
// Create test data err := os.Mkdir(resourcesControllerCfg.Path, 0777)
err := os.Mkdir("/tmp/tinyauth", 0755) require.NoError(t, err)
assert.NilError(t, err)
defer os.RemoveAll("/tmp/tinyauth")
file, err := os.Create("/tmp/tinyauth/test.txt") type testCase struct {
assert.NilError(t, err) description string
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
}
_, err = file.WriteString("This is a test file.") tests := []testCase{
assert.NilError(t, err) {
file.Close() description: "Ensure resources endpoint returns 200 OK for existing file",
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/resources/testfile.txt", nil)
router.ServeHTTP(recorder, req)
// Test existing file assert.Equal(t, 200, recorder.Code)
req := httptest.NewRequest("GET", "/resources/test.txt", nil) assert.Equal(t, "This is a test file.", recorder.Body.String())
recorder := httptest.NewRecorder() },
router.ServeHTTP(recorder, req) },
{
description: "Ensure resources endpoint returns 404 Not Found for non-existing file",
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/resources/nonexistent.txt", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code) assert.Equal(t, 404, recorder.Code)
assert.Equal(t, "This is a test file.", recorder.Body.String()) },
},
{
description: "Ensure resources controller denies path traversal",
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/resources/../somefile.txt", nil)
router.ServeHTTP(recorder, req)
// Test non-existing file assert.Equal(t, 404, recorder.Code)
req = httptest.NewRequest("GET", "/resources/nonexistent.txt", nil) },
recorder = httptest.NewRecorder() },
router.ServeHTTP(recorder, req) }
assert.Equal(t, 404, recorder.Code) testFilePath := resourcesControllerCfg.Path + "/testfile.txt"
err = os.WriteFile(testFilePath, []byte("This is a test file."), 0777)
require.NoError(t, err)
// Test directory traversal attack testFilePathParent := tempDir + "/somefile.txt"
req = httptest.NewRequest("GET", "/resources/../etc/passwd", nil) err = os.WriteFile(testFilePathParent, []byte("This file should not be accessible."), 0777)
recorder = httptest.NewRecorder() require.NoError(t, err)
router.ServeHTTP(recorder, req)
assert.Equal(t, 404, recorder.Code) for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
router := gin.Default()
group := router.Group("/")
gin.SetMode(gin.TestMode)
resourcesController := controller.NewResourcesController(resourcesControllerCfg, group)
resourcesController.SetupRoutes()
recorder := httptest.NewRecorder()
test.run(t, router, recorder)
})
}
} }

View File

@@ -2,305 +2,354 @@ package controller_test
import ( import (
"encoding/json" "encoding/json"
"net/http"
"net/http/httptest" "net/http/httptest"
"path"
"slices"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
"github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin" "github.com/stretchr/testify/require"
"github.com/pquerna/otp/totp"
"gotest.tools/v3/assert"
) )
var cookieValue string func TestUserController(t *testing.T) {
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ" tlog.NewTestLogger().Init()
tempDir := t.TempDir()
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { authServiceCfg := service.AuthServiceConfig{
tlog.NewSimpleLogger().Init()
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()
if middlewares != nil {
for _, m := range *middlewares {
router.Use(m)
}
}
group := router.Group("/api")
recorder := httptest.NewRecorder()
// Mock app
app := bootstrap.NewBootstrapApp(config.Config{})
// Database
db, err := app.SetupDatabase(":memory:")
assert.NilError(t, err)
// Queries
queries := repository.New(db)
// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{ Users: []config.User{
{ {
Username: "testuser", Username: "testuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
}, },
{ {
Username: "totpuser", Username: "totpuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
TotpSecret: totpSecret, TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
}, },
}, },
OauthWhitelist: []string{}, SessionExpiry: 10, // 10 seconds, useful for testing
SessionExpiry: 3600, CookieDomain: "example.com",
SessionMaxLifetime: 0, LoginTimeout: 10, // 10 seconds, useful for testing
SecureCookie: false, LoginMaxRetries: 3,
CookieDomain: "localhost", SessionCookieName: "tinyauth-session",
LoginTimeout: 300,
LoginMaxRetries: 3,
SessionCookieName: "tinyauth-session",
}, nil, nil, queries, &service.OAuthBrokerService{})
// Controller
ctrl := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: "localhost",
}, group, authService)
ctrl.SetupRoutes()
return router, recorder
}
func TestLoginHandler(t *testing.T) {
// Setup
router, recorder := setupUserController(t, nil)
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "test",
} }
loginReqJson, err := json.Marshal(loginReq) userControllerCfg := controller.UserControllerConfig{
assert.NilError(t, err) CookieDomain: "example.com",
// Test
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.Assert(t, cookie.Value != "")
cookieValue = cookie.Value
// Test invalid credentials
loginReq = controller.LoginRequest{
Username: "testuser",
Password: "invalid",
} }
loginReqJson, err = json.Marshal(loginReq) type testCase struct {
assert.NilError(t, err) description string
middlewares []gin.HandlerFunc
recorder = httptest.NewRecorder() run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
// Test totp required
loginReq = controller.LoginRequest{
Username: "totpuser",
Password: "test",
} }
loginReqJson, err = json.Marshal(loginReq) tests := []testCase{
assert.NilError(t, err) {
description: "Should be able to login with valid credentials",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "password",
}
loginReqBody, err := json.Marshal(loginReq)
assert.NoError(t, err)
recorder = httptest.NewRecorder() req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code) router.ServeHTTP(recorder, req)
loginResJson, err := json.Marshal(map[string]any{ assert.Equal(t, 200, recorder.Code)
"message": "TOTP required", assert.Len(t, recorder.Result().Cookies(), 1)
"status": 200,
"totpPending": true,
})
assert.NilError(t, err) cookie := recorder.Result().Cookies()[0]
assert.Equal(t, string(loginResJson), recorder.Body.String()) assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly)
// Test invalid json assert.Equal(t, "example.com", cookie.Domain)
recorder = httptest.NewRecorder() assert.Equal(t, 10, cookie.MaxAge)
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader("{invalid json}")) },
router.ServeHTTP(recorder, req)
assert.Equal(t, 400, recorder.Code)
// Test rate limiting
loginReq = controller.LoginRequest{
Username: "testuser",
Password: "invalid",
}
loginReqJson, err = json.Marshal(loginReq)
assert.NilError(t, err)
for range 5 {
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
router.ServeHTTP(recorder, req)
}
assert.Equal(t, 429, recorder.Code)
}
func TestLogoutHandler(t *testing.T) {
// Setup
router, recorder := setupUserController(t, nil)
// Test
req := httptest.NewRequest("POST", "/api/user/logout", nil)
req.AddCookie(&http.Cookie{
Name: "tinyauth-session",
Value: cookieValue,
})
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.Equal(t, "", cookie.Value)
assert.Equal(t, -1, cookie.MaxAge)
}
func TestTotpHandler(t *testing.T) {
// Setup
router, recorder := setupUserController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "totpuser",
Name: "totpuser",
Email: "totpuser@example.com",
IsLoggedIn: false,
OAuth: false,
Provider: "local",
TotpPending: true,
OAuthGroups: "",
TotpEnabled: true,
})
c.Next()
}, },
}) {
description: "Should reject login with invalid credentials",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "wrongpassword",
}
loginReqBody, err := json.Marshal(loginReq)
assert.NoError(t, err)
// Test req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
code, err := totp.GenerateCode(totpSecret, time.Now()) req.Header.Set("Content-Type", "application/json")
assert.NilError(t, err) router.ServeHTTP(recorder, req)
totpReq := controller.TotpRequest{ assert.Equal(t, 401, recorder.Code)
Code: code, assert.Len(t, recorder.Result().Cookies(), 0)
} assert.Contains(t, recorder.Body.String(), "Unauthorized")
},
totpReqJson, err := json.Marshal(totpReq)
assert.NilError(t, err)
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.Assert(t, cookie.Value != "")
// Test invalid json
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader("{invalid json}"))
router.ServeHTTP(recorder, req)
assert.Equal(t, 400, recorder.Code)
// Test rate limiting
totpReq = controller.TotpRequest{
Code: "000000",
}
totpReqJson, err = json.Marshal(totpReq)
assert.NilError(t, err)
for range 5 {
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
}
assert.Equal(t, 429, recorder.Code)
// Test invalid code
router, recorder = setupUserController(t, &[]gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "totpuser",
Name: "totpuser",
Email: "totpuser@example.com",
IsLoggedIn: false,
OAuth: false,
Provider: "local",
TotpPending: true,
OAuthGroups: "",
TotpEnabled: true,
})
c.Next()
}, },
}) {
description: "Should rate limit on 3 invalid attempts",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "wrongpassword",
}
loginReqBody, err := json.Marshal(loginReq)
assert.NoError(t, err)
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) for range 3 {
router.ServeHTTP(recorder, req) recorder := httptest.NewRecorder()
assert.Equal(t, 401, recorder.Code) req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
req.Header.Set("Content-Type", "application/json")
// Test no totp pending router.ServeHTTP(recorder, req)
router, recorder = setupUserController(t, &[]gin.HandlerFunc{
func(c *gin.Context) { assert.Equal(t, 401, recorder.Code)
c.Set("context", &config.UserContext{ assert.Len(t, recorder.Result().Cookies(), 0)
Username: "totpuser", assert.Contains(t, recorder.Body.String(), "Unauthorized")
Name: "totpuser", }
Email: "totpuser@example.com",
IsLoggedIn: false, // 4th attempt should be rate limited
OAuth: false, recorder = httptest.NewRecorder()
Provider: "local", req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
TotpPending: false, req.Header.Set("Content-Type", "application/json")
OAuthGroups: "",
TotpEnabled: false, router.ServeHTTP(recorder, req)
})
c.Next() assert.Equal(t, 429, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Too many failed login attempts.")
},
}, },
{
description: "Should not allow full login with totp",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
loginReq := controller.LoginRequest{
Username: "totpuser",
Password: "password",
}
loginReqBody, err := json.Marshal(loginReq)
assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
decodedBody := make(map[string]any)
err = json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
assert.NoError(t, err)
assert.Equal(t, decodedBody["totpPending"], true)
// should set the session cookie
assert.Len(t, recorder.Result().Cookies(), 1)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain)
assert.Equal(t, 3600, cookie.MaxAge) // 1 hour, default for totp pending sessions
},
},
{
description: "Should be able to logout",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
// First login to get a session cookie
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "password",
}
loginReqBody, err := json.Marshal(loginReq)
assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Len(t, recorder.Result().Cookies(), 1)
cookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", cookie.Name)
// Now logout using the session cookie
recorder = httptest.NewRecorder()
req = httptest.NewRequest("POST", "/api/user/logout", nil)
req.AddCookie(cookie)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Len(t, recorder.Result().Cookies(), 1)
logoutCookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", logoutCookie.Name)
assert.Equal(t, "", logoutCookie.Value)
assert.Equal(t, -1, logoutCookie.MaxAge) // MaxAge -1 means delete cookie
},
},
{
description: "Should be able to login with totp",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
assert.NoError(t, err)
totpReq := controller.TotpRequest{
Code: code,
}
totpReqBody, err := json.Marshal(totpReq)
assert.NoError(t, err)
recorder = httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Len(t, recorder.Result().Cookies(), 1)
// should set a new session cookie with totp pending removed
totpCookie := recorder.Result().Cookies()[0]
assert.Equal(t, "tinyauth-session", totpCookie.Name)
assert.True(t, totpCookie.HttpOnly)
assert.Equal(t, "example.com", totpCookie.Domain)
assert.Equal(t, 10, totpCookie.MaxAge) // should use the regular session expiry time
},
},
{
description: "Totp should rate limit on multiple invalid attempts",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
for range 3 {
totpReq := controller.TotpRequest{
Code: "000000", // invalid code
}
totpReqBody, err := json.Marshal(totpReq)
assert.NoError(t, err)
recorder = httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Unauthorized")
}
// 4th attempt should be rate limited
recorder = httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(`{"code":"000000"}`)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, 429, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Too many failed TOTP attempts.")
},
},
}
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err)
queries := repository.New(db)
docker := service.NewDockerService()
err = docker.Init()
require.NoError(t, err)
ldap := service.NewLdapService(service.LdapServiceConfig{})
err = ldap.Init()
require.NoError(t, err)
broker := service.NewOAuthBrokerService(oauthBrokerCfgs)
err = broker.Init()
require.NoError(t, err)
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
err = authService.Init()
require.NoError(t, err)
beforeEach := func() {
// Clear failed login attempts before each test
authService.ClearRateLimitsTestingOnly()
}
setTotpMiddlewareOverrides := []string{
"Should be able to login with totp",
"Totp should rate limit on multiple invalid attempts",
}
for _, test := range tests {
beforeEach()
t.Run(test.description, func(t *testing.T) {
router := gin.Default()
for _, middleware := range test.middlewares {
router.Use(middleware)
}
// Gin is stupid and doesn't allow setting a middleware after the groups
// so we need to do some stupid overrides here
if slices.Contains(setTotpMiddlewareOverrides, test.description) {
// Assuming the cookie is set, it should be picked up by the
// context middleware
router.Use(func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "totpuser",
Name: "Totpuser",
Email: "totpuser@example.com",
Provider: "local",
TotpPending: true,
TotpEnabled: true,
})
})
}
group := router.Group("/api")
gin.SetMode(gin.TestMode)
userController := controller.NewUserController(userControllerCfg, group, authService)
userController.SetupRoutes()
recorder := httptest.NewRecorder()
test.run(t, router, recorder)
})
}
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
}) })
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
} }

View File

@@ -0,0 +1,131 @@
package controller_test
import (
"encoding/json"
"fmt"
"net/http/httptest"
"path"
"testing"
"github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWellKnownController(t *testing.T) {
tlog.NewTestLogger().Init()
tempDir := t.TempDir()
oidcServiceCfg := service.OIDCServiceConfig{
Clients: map[string]config.OIDCClientConfig{
"test": {
ClientID: "some-client-id",
ClientSecret: "some-client-secret",
TrustedRedirectURIs: []string{"https://test.example.com/callback"},
Name: "Test Client",
},
},
PrivateKeyPath: path.Join(tempDir, "key.pem"),
PublicKeyPath: path.Join(tempDir, "key.pub"),
Issuer: "https://tinyauth.example.com",
SessionExpiry: 500,
}
type testCase struct {
description string
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
}
tests := []testCase{
{
description: "Ensure well-known endpoint returns correct OIDC configuration",
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
res := controller.OpenIDConnectConfiguration{}
err := json.Unmarshal(recorder.Body.Bytes(), &res)
assert.NoError(t, err)
expected := controller.OpenIDConnectConfiguration{
Issuer: oidcServiceCfg.Issuer,
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", oidcServiceCfg.Issuer),
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", oidcServiceCfg.Issuer),
UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", oidcServiceCfg.Issuer),
JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", oidcServiceCfg.Issuer),
ScopesSupported: service.SupportedScopes,
ResponseTypesSupported: service.SupportedResponseTypes,
GrantTypesSupported: service.SupportedGrantTypes,
SubjectTypesSupported: []string{"pairwise"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"},
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
}
assert.Equal(t, expected, res)
},
},
{
description: "Ensure well-known endpoint returns correct JWKS",
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/.well-known/jwks.json", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
decodedBody := make(map[string]any)
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
assert.NoError(t, err)
keys, ok := decodedBody["keys"].([]any)
assert.True(t, ok)
assert.Len(t, keys, 1)
keyData, ok := keys[0].(map[string]any)
assert.True(t, ok)
assert.Equal(t, "RSA", keyData["kty"])
assert.Equal(t, "sig", keyData["use"])
assert.Equal(t, "RS256", keyData["alg"])
},
},
}
app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err)
queries := repository.New(db)
oidcService := service.NewOIDCService(oidcServiceCfg, queries)
err = oidcService.Init()
require.NoError(t, err)
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
router := gin.Default()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
wellKnownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{}, oidcService, router)
wellKnownController.SetupRoutes()
test.run(t, router, recorder)
})
}
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
})
}

View File

@@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"slices"
"strings" "strings"
"time" "time"
@@ -13,7 +12,25 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var OIDCIgnorePaths = []string{"/api/oidc/token", "/api/oidc/userinfo"} // Gin won't let us set a middleware on a specific route (at least it doesn't work,
// see https://github.com/gin-gonic/gin/issues/531) so we have to do some hackery
var (
contextSkipPathsPrefix = []string{
"GET /api/context/app",
"GET /api/healthz",
"HEAD /api/healthz",
"GET /api/oauth/url",
"GET /api/oauth/callback",
"GET /api/oidc/clients",
"POST /api/oidc/token",
"GET /api/oidc/userinfo",
"POST /api/oidc/userinfo",
"GET /resources",
"POST /api/user/login",
"GET /.well-known/openid-configuration",
"GET /.well-known/jwks.json",
}
)
type ContextMiddlewareConfig struct { type ContextMiddlewareConfig struct {
CookieDomain string CookieDomain string
@@ -39,9 +56,7 @@ func (m *ContextMiddleware) Init() error {
func (m *ContextMiddleware) Middleware() gin.HandlerFunc { func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// There is no point in trying to get credentials if it's an OIDC endpoint if m.isIgnorePath(c.Request.Method + " " + c.Request.URL.Path) {
path := c.Request.URL.Path
if slices.Contains(OIDCIgnorePaths, strings.TrimSuffix(path, "/")) {
c.Next() c.Next()
return return
} }
@@ -224,3 +239,12 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
c.Next() c.Next()
} }
} }
func (m *ContextMiddleware) isIgnorePath(path string) bool {
for _, prefix := range contextSkipPathsPrefix {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}

View File

@@ -8,10 +8,11 @@ import (
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
) )
// See context middleware for explanation of why we have to do this
var ( var (
loggerSkipPathsPrefix = []string{ loggerSkipPathsPrefix = []string{
"GET /api/health", "GET /api/healthz",
"HEAD /api/health", "HEAD /api/healthz",
"GET /favicon.ico", "GET /favicon.ico",
} }
) )

View File

@@ -5,13 +5,14 @@
package repository package repository
type OidcCode struct { type OidcCode struct {
Sub string Sub string
CodeHash string CodeHash string
Scope string Scope string
RedirectURI string RedirectURI string
ClientID string ClientID string
ExpiresAt int64 ExpiresAt int64
Nonce string Nonce string
CodeChallenge string
} }
type OidcToken struct { type OidcToken struct {

View File

@@ -17,21 +17,23 @@ INSERT INTO "oidc_codes" (
"redirect_uri", "redirect_uri",
"client_id", "client_id",
"expires_at", "expires_at",
"nonce" "nonce",
"code_challenge"
) VALUES ( ) VALUES (
?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?
) )
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
` `
type CreateOidcCodeParams struct { type CreateOidcCodeParams struct {
Sub string Sub string
CodeHash string CodeHash string
Scope string Scope string
RedirectURI string RedirectURI string
ClientID string ClientID string
ExpiresAt int64 ExpiresAt int64
Nonce string Nonce string
CodeChallenge string
} }
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) { func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
@@ -43,6 +45,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
arg.ClientID, arg.ClientID,
arg.ExpiresAt, arg.ExpiresAt,
arg.Nonce, arg.Nonce,
arg.CodeChallenge,
) )
var i OidcCode var i OidcCode
err := row.Scan( err := row.Scan(
@@ -53,6 +56,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
&i.ClientID, &i.ClientID,
&i.ExpiresAt, &i.ExpiresAt,
&i.Nonce, &i.Nonce,
&i.CodeChallenge,
) )
return i, err return i, err
} }
@@ -156,7 +160,7 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
DELETE FROM "oidc_codes" DELETE FROM "oidc_codes"
WHERE "expires_at" < ? WHERE "expires_at" < ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
` `
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) { func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
@@ -176,6 +180,7 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
&i.ClientID, &i.ClientID,
&i.ExpiresAt, &i.ExpiresAt,
&i.Nonce, &i.Nonce,
&i.CodeChallenge,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -286,7 +291,7 @@ func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
const getOidcCode = `-- name: GetOidcCode :one const getOidcCode = `-- name: GetOidcCode :one
DELETE FROM "oidc_codes" DELETE FROM "oidc_codes"
WHERE "code_hash" = ? WHERE "code_hash" = ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
` `
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) { func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
@@ -300,6 +305,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
&i.ClientID, &i.ClientID,
&i.ExpiresAt, &i.ExpiresAt,
&i.Nonce, &i.Nonce,
&i.CodeChallenge,
) )
return i, err return i, err
} }
@@ -307,7 +313,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
DELETE FROM "oidc_codes" DELETE FROM "oidc_codes"
WHERE "sub" = ? WHERE "sub" = ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
` `
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) { func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
@@ -321,12 +327,13 @@ func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, e
&i.ClientID, &i.ClientID,
&i.ExpiresAt, &i.ExpiresAt,
&i.Nonce, &i.Nonce,
&i.CodeChallenge,
) )
return i, err return i, err
} }
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes" SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
WHERE "sub" = ? WHERE "sub" = ?
` `
@@ -341,12 +348,13 @@ func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcC
&i.ClientID, &i.ClientID,
&i.ExpiresAt, &i.ExpiresAt,
&i.Nonce, &i.Nonce,
&i.CodeChallenge,
) )
return i, err return i, err
} }
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes" SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
WHERE "code_hash" = ? WHERE "code_hash" = ?
` `
@@ -361,6 +369,7 @@ func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcC
&i.ClientID, &i.ClientID,
&i.ExpiresAt, &i.ExpiresAt,
&i.Nonce, &i.Nonce,
&i.CodeChallenge,
) )
return i, err return i, err
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@@ -27,12 +28,26 @@ const MaxOAuthPendingSessions = 256
const OAuthCleanupCount = 16 const OAuthCleanupCount = 16
const MaxLoginAttemptRecords = 256 const MaxLoginAttemptRecords = 256
// slightly modified version of the AuthorizeRequest from the OIDC service to basically accept all
// parameters and pass them to the authorize page if needed
type OAuthURLParams struct {
Scope string `form:"scope" url:"scope"`
ResponseType string `form:"response_type" url:"response_type"`
ClientID string `form:"client_id" url:"client_id"`
RedirectURI string `form:"redirect_uri" url:"redirect_uri"`
State string `form:"state" url:"state"`
Nonce string `form:"nonce" url:"nonce"`
CodeChallenge string `form:"code_challenge" url:"code_challenge"`
CodeChallengeMethod string `form:"code_challenge_method" url:"code_challenge_method"`
}
type OAuthPendingSession struct { type OAuthPendingSession struct {
State string State string
Verifier string Verifier string
Token *oauth2.Token Token *oauth2.Token
Service *OAuthServiceImpl Service *OAuthServiceImpl
ExpiresAt time.Time ExpiresAt time.Time
CallbackParams OAuthURLParams
} }
type LdapGroupsCache struct { type LdapGroupsCache struct {
@@ -78,6 +93,8 @@ type AuthService struct {
queries *repository.Queries queries *repository.Queries
oauthBroker *OAuthBrokerService oauthBroker *OAuthBrokerService
lockdown *Lockdown lockdown *Lockdown
lockdownCtx context.Context
lockdownCancelFunc context.CancelFunc
} }
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService { func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
@@ -595,7 +612,7 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
return false return false
} }
func (auth *AuthService) NewOAuthSession(serviceName string) (string, OAuthPendingSession, error) { func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
auth.ensureOAuthSessionLimit() auth.ensureOAuthSessionLimit()
service, ok := auth.oauthBroker.GetService(serviceName) service, ok := auth.oauthBroker.GetService(serviceName)
@@ -614,10 +631,11 @@ func (auth *AuthService) NewOAuthSession(serviceName string) (string, OAuthPendi
verifier := service.NewRandom() verifier := service.NewRandom()
session := OAuthPendingSession{ session := OAuthPendingSession{
State: state, State: state,
Verifier: verifier, Verifier: verifier,
Service: &service, Service: &service,
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: time.Now().Add(1 * time.Hour),
CallbackParams: params,
} }
auth.oauthMutex.Lock() auth.oauthMutex.Lock()
@@ -628,7 +646,7 @@ func (auth *AuthService) NewOAuthSession(serviceName string) (string, OAuthPendi
} }
func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) { func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
session, err := auth.getOAuthPendingSession(sessionId) session, err := auth.GetOAuthPendingSession(sessionId)
if err != nil { if err != nil {
return "", err return "", err
@@ -638,7 +656,7 @@ func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
} }
func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) { func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) {
session, err := auth.getOAuthPendingSession(sessionId) session, err := auth.GetOAuthPendingSession(sessionId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -658,7 +676,7 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
} }
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, error) { func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, error) {
session, err := auth.getOAuthPendingSession(sessionId) session, err := auth.GetOAuthPendingSession(sessionId)
if err != nil { if err != nil {
return config.Claims{}, err return config.Claims{}, err
@@ -678,7 +696,7 @@ func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, erro
} }
func (auth *AuthService) GetOAuthService(sessionId string) (OAuthServiceImpl, error) { func (auth *AuthService) GetOAuthService(sessionId string) (OAuthServiceImpl, error) {
session, err := auth.getOAuthPendingSession(sessionId) session, err := auth.GetOAuthPendingSession(sessionId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -712,7 +730,7 @@ func (auth *AuthService) CleanupOAuthSessionsRoutine() {
} }
} }
func (auth *AuthService) getOAuthPendingSession(sessionId string) (*OAuthPendingSession, error) { func (auth *AuthService) GetOAuthPendingSession(sessionId string) (*OAuthPendingSession, error) {
auth.ensureOAuthSessionLimit() auth.ensureOAuthSessionLimit()
auth.oauthMutex.RLock() auth.oauthMutex.RLock()
@@ -770,6 +788,11 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
} }
func (auth *AuthService) lockdownMode() { func (auth *AuthService) lockdownMode() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
auth.lockdownCtx = ctx
auth.lockdownCancelFunc = cancel
auth.loginMutex.Lock() auth.loginMutex.Lock()
tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.") tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.")
@@ -788,7 +811,12 @@ func (auth *AuthService) lockdownMode() {
auth.loginMutex.Unlock() auth.loginMutex.Unlock()
<-timer.C select {
case <-timer.C:
// Timer expired, end lockdown
case <-ctx.Done():
// Context cancelled, end lockdown
}
auth.loginMutex.Lock() auth.loginMutex.Lock()
@@ -796,3 +824,13 @@ func (auth *AuthService) lockdownMode() {
auth.lockdown = nil auth.lockdown = nil
auth.loginMutex.Unlock() auth.loginMutex.Unlock()
} }
// Function only used for testing - do not use in prod!
func (auth *AuthService) ClearRateLimitsTestingOnly() {
auth.loginMutex.Lock()
auth.loginAttempts = make(map[string]*LoginAttempt)
if auth.lockdown != nil {
auth.lockdownCancelFunc()
}
auth.loginMutex.Unlock()
}

View File

@@ -10,6 +10,7 @@ import (
type OAuthServiceImpl interface { type OAuthServiceImpl interface {
Name() string Name() string
ID() string
NewRandom() string NewRandom() string
GetAuthURL(state string, verifier string) string GetAuthURL(state string, verifier string) string
GetToken(code string, verifier string) (*oauth2.Token, error) GetToken(code string, verifier string) (*oauth2.Token, error)
@@ -39,7 +40,7 @@ func (broker *OAuthBrokerService) Init() error {
broker.services[name] = presetFunc(cfg) broker.services[name] = presetFunc(cfg)
tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset") tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset")
} else { } else {
broker.services[name] = NewOAuthService(cfg) broker.services[name] = NewOAuthService(cfg, name)
tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from config") tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from config")
} }
} }

View File

@@ -11,7 +11,7 @@ func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
config.AuthURL = endpoints.Google.AuthURL config.AuthURL = endpoints.Google.AuthURL
config.TokenURL = endpoints.Google.TokenURL config.TokenURL = endpoints.Google.TokenURL
config.UserinfoURL = "https://openidconnect.googleapis.com/v1/userinfo" config.UserinfoURL = "https://openidconnect.googleapis.com/v1/userinfo"
return NewOAuthService(config) return NewOAuthService(config, "google")
} }
func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService { func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
@@ -19,5 +19,5 @@ func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
config.Scopes = scopes config.Scopes = scopes
config.AuthURL = endpoints.GitHub.AuthURL config.AuthURL = endpoints.GitHub.AuthURL
config.TokenURL = endpoints.GitHub.TokenURL config.TokenURL = endpoints.GitHub.TokenURL
return NewOAuthService(config).WithUserinfoExtractor(githubExtractor) return NewOAuthService(config, "github").WithUserinfoExtractor(githubExtractor)
} }

View File

@@ -17,9 +17,10 @@ type OAuthService struct {
config *oauth2.Config config *oauth2.Config
ctx context.Context ctx context.Context
userinfoExtractor UserinfoExtractor userinfoExtractor UserinfoExtractor
id string
} }
func NewOAuthService(config config.OAuthServiceConfig) *OAuthService { func NewOAuthService(config config.OAuthServiceConfig, id string) *OAuthService {
httpClient := &http.Client{ httpClient := &http.Client{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
@@ -45,6 +46,7 @@ func NewOAuthService(config config.OAuthServiceConfig) *OAuthService {
}, },
ctx: ctx, ctx: ctx,
userinfoExtractor: defaultExtractor, userinfoExtractor: defaultExtractor,
id: id,
} }
} }
@@ -57,6 +59,10 @@ func (s *OAuthService) Name() string {
return s.serviceCfg.Name return s.serviceCfg.Name
} }
func (s *OAuthService) ID() string {
return s.id
}
func (s *OAuthService) NewRandom() string { func (s *OAuthService) NewRandom() string {
// The generate verifier function just creates a random string, // The generate verifier function just creates a random string,
// so we can use it to generate a random state as well // so we can use it to generate a random state as well

View File

@@ -75,12 +75,14 @@ type TokenResponse struct {
} }
type AuthorizeRequest struct { type AuthorizeRequest struct {
Scope string `json:"scope" binding:"required"` Scope string `json:"scope" binding:"required"`
ResponseType string `json:"response_type" binding:"required"` ResponseType string `json:"response_type" binding:"required"`
ClientID string `json:"client_id" binding:"required"` ClientID string `json:"client_id" binding:"required"`
RedirectURI string `json:"redirect_uri" binding:"required"` RedirectURI string `json:"redirect_uri" binding:"required"`
State string `json:"state"` State string `json:"state"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
} }
type OIDCServiceConfig struct { type OIDCServiceConfig struct {
@@ -293,6 +295,13 @@ func (service *OIDCService) ValidateAuthorizeParams(req AuthorizeRequest) error
return errors.New("invalid_request_uri") return errors.New("invalid_request_uri")
} }
// PKCE code challenge method if set
if req.CodeChallenge != "" && req.CodeChallengeMethod != "" {
if req.CodeChallengeMethod != "S256" && req.CodeChallengeMethod != "plain" {
return errors.New("invalid_request")
}
}
return nil return nil
} }
@@ -306,8 +315,7 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
// Fixed 10 minutes // Fixed 10 minutes
expiresAt := time.Now().Add(time.Minute * time.Duration(10)).Unix() expiresAt := time.Now().Add(time.Minute * time.Duration(10)).Unix()
// Insert the code into the database entry := repository.CreateOidcCodeParams{
_, err := service.queries.CreateOidcCode(c, repository.CreateOidcCodeParams{
Sub: sub, Sub: sub,
CodeHash: service.Hash(code), CodeHash: service.Hash(code),
// Here it's safe to split and trust the output since, we validated the scopes before // Here it's safe to split and trust the output since, we validated the scopes before
@@ -316,7 +324,19 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
ClientID: req.ClientID, ClientID: req.ClientID,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Nonce: req.Nonce, Nonce: req.Nonce,
}) }
if req.CodeChallenge != "" {
if req.CodeChallengeMethod == "S256" {
entry.CodeChallenge = req.CodeChallenge
} else {
entry.CodeChallenge = service.hashAndEncodePKCE(req.CodeChallenge)
tlog.App.Warn().Msg("Received plain PKCE code challenge, it's recommended to use S256 for better security")
}
}
// Insert the code into the database
_, err := service.queries.CreateOidcCode(c, entry)
return err return err
} }
@@ -728,3 +748,16 @@ func (service *OIDCService) GetJWK() ([]byte, error) {
return jwk.Public().MarshalJSON() return jwk.Public().MarshalJSON()
} }
func (service *OIDCService) ValidatePKCE(codeChallenge string, codeVerifier string) bool {
if codeChallenge == "" {
return true
}
return codeChallenge == service.hashAndEncodePKCE(codeVerifier)
}
func (service *OIDCService) hashAndEncodePKCE(codeVerifier string) string {
hasher := sha256.New()
hasher.Write([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
}

View File

@@ -25,12 +25,6 @@ func TestGetRootDomain(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Domain with no subdomain
domain = "http://tinyauth.app"
expected = "tinyauth.app"
_, err = utils.GetCookieDomain(domain)
assert.Error(t, err, "invalid app url, must be at least second level domain")
// Invalid domain (only TLD) // Invalid domain (only TLD)
domain = "com" domain = "com"
_, err = utils.GetCookieDomain(domain) _, err = utils.GetCookieDomain(domain)

View File

@@ -1,7 +1,7 @@
package decoders package decoders
import ( import (
"github.com/traefik/paerser/parser" "github.com/tinyauthapp/paerser/parser"
) )
func DecodeLabels[T any](labels map[string]string, root string) (T, error) { func DecodeLabels[T any](labels map[string]string, root string) (T, error) {

View File

@@ -6,8 +6,8 @@ import (
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/traefik/paerser/env" "github.com/tinyauthapp/paerser/env"
) )
type EnvLoader struct{} type EnvLoader struct{}

View File

@@ -4,9 +4,9 @@ import (
"os" "os"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/traefik/paerser/file" "github.com/tinyauthapp/paerser/file"
"github.com/traefik/paerser/flag" "github.com/tinyauthapp/paerser/flag"
) )
type FileLoader struct{} type FileLoader struct{}

View File

@@ -3,8 +3,8 @@ package loaders
import ( import (
"fmt" "fmt"
"github.com/traefik/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/traefik/paerser/flag" "github.com/tinyauthapp/paerser/flag"
) )
type FlagLoader struct{} type FlagLoader struct{}

View File

@@ -55,6 +55,17 @@ func NewSimpleLogger() *Logger {
}) })
} }
func NewTestLogger() *Logger {
return NewLogger(config.LogConfig{
Level: "trace",
Streams: config.LogStreams{
HTTP: config.LogStreamConfig{Enabled: true},
App: config.LogStreamConfig{Enabled: true},
Audit: config.LogStreamConfig{Enabled: true},
},
})
}
func (l *Logger) Init() { func (l *Logger) Init() {
Audit = l.Audit Audit = l.Audit
HTTP = l.HTTP HTTP = l.HTTP

Submodule paerser deleted from 7e1b633ba9

View File

@@ -1,95 +0,0 @@
diff --git a/env/env_test.go b/env/env_test.go
index 7045569..365dc00 100644
--- a/env/env_test.go
+++ b/env/env_test.go
@@ -166,6 +166,38 @@ func TestDecode(t *testing.T) {
Foo: &struct{ Field string }{},
},
},
+ {
+ desc: "map under the root key",
+ environ: []string{"TRAEFIK_FOO_BAR_FOOBAR_BARFOO=foo"},
+ element: &struct {
+ Foo map[string]struct {
+ Foobar struct {
+ Barfoo string
+ }
+ }
+ }{},
+ expected: &struct {
+ Foo map[string]struct {
+ Foobar struct {
+ Barfoo string
+ }
+ }
+ }{
+ Foo: map[string]struct {
+ Foobar struct {
+ Barfoo string
+ }
+ }{
+ "bar": {
+ Foobar: struct {
+ Barfoo string
+ }{
+ Barfoo: "foo",
+ },
+ },
+ },
+ },
+ },
}
for _, test := range testCases {
diff --git a/parser/nodes_metadata.go b/parser/nodes_metadata.go
index 36946c1..0279705 100644
--- a/parser/nodes_metadata.go
+++ b/parser/nodes_metadata.go
@@ -75,8 +75,13 @@ func (m metadata) add(rootType reflect.Type, node *Node) error {
node.Kind = fType.Kind()
node.Tag = field.Tag
- if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Pointer && fType.Elem().Kind() == reflect.Struct ||
- fType.Kind() == reflect.Map {
+ if node.Kind == reflect.String && len(node.Children) > 0 {
+ fType = reflect.TypeOf(struct{}{})
+ node.Kind = reflect.Struct
+ }
+
+ if node.Kind == reflect.Struct || node.Kind == reflect.Pointer && fType.Elem().Kind() == reflect.Struct ||
+ node.Kind == reflect.Map {
if len(node.Children) == 0 && !(field.Tag.Get(m.TagName) == TagLabelAllowEmpty || field.Tag.Get(m.TagName) == "-") {
return fmt.Errorf("%s cannot be a standalone element (type %s)", node.Name, fType)
}
@@ -90,11 +95,11 @@ func (m metadata) add(rootType reflect.Type, node *Node) error {
return nil
}
- if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Pointer && fType.Elem().Kind() == reflect.Struct {
+ if node.Kind == reflect.Struct || node.Kind == reflect.Pointer && fType.Elem().Kind() == reflect.Struct {
return m.browseChildren(fType, node)
}
- if fType.Kind() == reflect.Map {
+ if node.Kind == reflect.Map {
if fType.Elem().Kind() == reflect.Interface {
addRawValue(node)
return nil
@@ -115,7 +120,7 @@ func (m metadata) add(rootType reflect.Type, node *Node) error {
return nil
}
- if fType.Kind() == reflect.Slice {
+ if node.Kind == reflect.Slice {
if m.AllowSliceAsStruct && field.Tag.Get(TagLabelSliceAsStruct) != "" {
return m.browseChildren(fType.Elem(), node)
}
@@ -129,7 +134,7 @@ func (m metadata) add(rootType reflect.Type, node *Node) error {
return nil
}
- return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind())
+ return fmt.Errorf("invalid node %s: %v", node.Name, node.Kind)
}
func (m metadata) findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) {

View File

@@ -6,9 +6,10 @@ INSERT INTO "oidc_codes" (
"redirect_uri", "redirect_uri",
"client_id", "client_id",
"expires_at", "expires_at",
"nonce" "nonce",
"code_challenge"
) VALUES ( ) VALUES (
?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?
) )
RETURNING *; RETURNING *;

View File

@@ -5,7 +5,8 @@ CREATE TABLE IF NOT EXISTS "oidc_codes" (
"redirect_uri" TEXT NOT NULL, "redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL, "client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL, "expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT "" "nonce" TEXT DEFAULT "",
"code_challenge" TEXT DEFAULT ""
); );
CREATE TABLE IF NOT EXISTS "oidc_tokens" ( CREATE TABLE IF NOT EXISTS "oidc_tokens" (

View File

@@ -26,3 +26,5 @@ sql:
go_type: "string" go_type: "string"
- column: "oidc_tokens.nonce" - column: "oidc_tokens.nonce"
go_type: "string" go_type: "string"
- column: "oidc_codes.code_challenge"
go_type: "string"