Compare commits

..

20 Commits

Author SHA1 Message Date
Elias Schneider
c9308472a9 release: 1.14.0 2025-10-24 13:47:58 +02:00
Elias Schneider
6362ff9861 chore: upgrade dependencies 2025-10-24 12:18:38 +02:00
Elias Schneider
10d640385f fix: prevent page flickering on redirection based on auth state 2025-10-24 12:12:42 +02:00
Elias Schneider
47927d1574 fix: make pkce requirement visible in the oidc form if client is public 2025-10-24 10:57:44 +02:00
Elias Schneider
b356cef766 fix: only animate login background on initial page load 2025-10-24 10:53:51 +02:00
Elias Schneider
9fc45930a8 chore(translations): update translations via Crowdin (#1033) 2025-10-24 09:58:00 +02:00
Kyle Mendell
028d1c858e feat: add support for dark mode oidc client icons (#1039)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-10-24 09:57:12 +02:00
Alessandro (Ale) Segala
eb3963d0fc fix: use constant time comparisons when validating PKCE challenges (#1047) 2025-10-24 08:30:50 +02:00
dependabot[bot]
35d913f905 chore(deps-dev): bump vite from 7.0.7 to 7.0.8 in the npm_and_yarn group across 1 directory (#1042)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-21 18:04:28 -05:00
github-actions[bot]
32485f4c7c chore: update AAGUIDs (#1041)
Co-authored-by: stonith404 <58886915+stonith404@users.noreply.github.com>
2025-10-20 08:43:27 +02:00
Elias Schneider
ceb38b0825 chore(translations): update translations via Crowdin (#1025) 2025-10-16 08:28:13 +02:00
dependabot[bot]
c0b6ede5be chore(deps): bump sveltekit-superforms from 2.27.1 to 2.27.4 in the npm_and_yarn group across 1 directory (#1031)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-16 08:27:52 +02:00
Elias Schneider
c20e93b55c feat: add various improvements to the table component (#961)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
2025-10-13 09:12:55 +00:00
Elias Schneider
24ca6a106d chore(translations): update translations via Crowdin (#1014) 2025-10-12 09:58:22 -05:00
Elias Schneider
9f0aa55be6 fix: ignore trailing slash in URL 2025-10-09 20:27:15 +02:00
Kyle Mendell
068fcc65a6 chore(translations): add Japanese files 2025-10-07 18:29:13 -05:00
Elias Schneider
f2dfb3da5d release: 1.13.1 2025-10-07 08:21:41 +02:00
Elias Schneider
cbf0e3117d fix: mark any callback url as valid if they contain a wildcard (#1006) 2025-10-07 08:18:53 +02:00
CzBiX
694f266dea fix: uploading a client logo with an URL fails (#1008) 2025-10-06 10:37:43 -05:00
Kyle Mendell
29fc185376 chore: cleanup root of repo, update workflow actions (#1003)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-10-05 14:49:06 -05:00
146 changed files with 4857 additions and 3527 deletions

View File

@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: backend/go.mod go-version-file: backend/go.mod

View File

@@ -19,22 +19,20 @@ jobs:
attestations: write attestations: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 22
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: 'backend/go.mod' go-version-file: "backend/go.mod"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@@ -74,7 +72,7 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ env.DOCKER_IMAGE_NAME }}:next tags: ${{ env.DOCKER_IMAGE_NAME }}:next
file: Dockerfile-prebuilt file: docker/Dockerfile-prebuilt
- name: Build and push container image (distroless) - name: Build and push container image (distroless)
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
id: container-build-push-distroless id: container-build-push-distroless
@@ -83,16 +81,16 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ env.DOCKER_IMAGE_NAME }}:next-distroless tags: ${{ env.DOCKER_IMAGE_NAME }}:next-distroless
file: Dockerfile-distroless file: docker/Dockerfile-distroless
- name: Container image attestation - name: Container image attestation
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: '${{ env.DOCKER_IMAGE_NAME }}' subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
subject-digest: ${{ steps.build-push-image.outputs.digest }} subject-digest: ${{ steps.build-push-image.outputs.digest }}
push-to-registry: true push-to-registry: true
- name: Container image attestation (distroless) - name: Container image attestation (distroless)
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: '${{ env.DOCKER_IMAGE_NAME }}' subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }} subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
push-to-registry: true push-to-registry: true

View File

@@ -3,15 +3,15 @@ on:
push: push:
branches: [main] branches: [main]
paths-ignore: paths-ignore:
- 'docs/**' - "docs/**"
- '**.md' - "**.md"
- '.github/**' - ".github/**"
pull_request: pull_request:
branches: [main] branches: [main]
paths-ignore: paths-ignore:
- 'docs/**' - "docs/**"
- '**.md' - "**.md"
- '.github/**' - ".github/**"
jobs: jobs:
build: build:
@@ -22,7 +22,7 @@ jobs:
actions: write actions: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -30,6 +30,8 @@ jobs:
- name: Build and export - name: Build and export
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: .
file: docker/Dockerfile
push: false push: false
load: false load: false
tags: pocket-id:test tags: pocket-id:test
@@ -57,16 +59,15 @@ jobs:
matrix: matrix:
db: [sqlite, postgres] db: [sqlite, postgres]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - name: Setup Node.js
uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 22
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
- name: Cache Playwright Browsers - name: Cache Playwright Browsers
uses: actions/cache@v3 uses: actions/cache@v3

View File

@@ -3,7 +3,7 @@ name: Release
on: on:
push: push:
tags: tags:
- 'v*.*.*' - "v*.*.*"
jobs: jobs:
build: build:
@@ -19,14 +19,12 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 22
cache: 'pnpm' - uses: actions/setup-go@v6
cache-dependency-path: pnpm-lock.yaml
- uses: actions/setup-go@v5
with: with:
go-version-file: 'backend/go.mod' go-version-file: "backend/go.mod"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -81,7 +79,7 @@ jobs:
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
file: Dockerfile-prebuilt file: docker/Dockerfile-prebuilt
- name: Build and push container image (distroless) - name: Build and push container image (distroless)
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
id: container-build-push-distroless id: container-build-push-distroless
@@ -91,21 +89,21 @@ jobs:
push: true push: true
tags: ${{ steps.meta-distroless.outputs.tags }} tags: ${{ steps.meta-distroless.outputs.tags }}
labels: ${{ steps.meta-distroless.outputs.labels }} labels: ${{ steps.meta-distroless.outputs.labels }}
file: Dockerfile-distroless file: docker/Dockerfile-distroless
- name: Binary attestation - name: Binary attestation
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-path: 'backend/.bin/pocket-id-**' subject-path: "backend/.bin/pocket-id-**"
- name: Container image attestation - name: Container image attestation
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: '${{ env.DOCKER_IMAGE_NAME }}' subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
subject-digest: ${{ steps.container-build-push.outputs.digest }} subject-digest: ${{ steps.container-build-push.outputs.digest }}
push-to-registry: true push-to-registry: true
- name: Container image attestation (distroless) - name: Container image attestation (distroless)
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: '${{ env.DOCKER_IMAGE_NAME }}' subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }} subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
push-to-registry: true push-to-registry: true
- name: Upload binaries to release - name: Upload binaries to release
@@ -122,6 +120,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Mark release as published - name: Mark release as published
run: gh release edit ${{ github.ref_name }} --draft=false run: gh release edit ${{ github.ref_name }} --draft=false

View File

@@ -4,21 +4,21 @@ on:
push: push:
branches: [main] branches: [main]
paths: paths:
- 'frontend/src/**' - "frontend/src/**"
- '.github/svelte-check-matcher.json' - ".github/svelte-check-matcher.json"
- 'frontend/package.json' - "frontend/package.json"
- 'frontend/package-lock.json' - "frontend/package-lock.json"
- 'frontend/tsconfig.json' - "frontend/tsconfig.json"
- 'frontend/svelte.config.js' - "frontend/svelte.config.js"
pull_request: pull_request:
branches: [main] branches: [main]
paths: paths:
- 'frontend/src/**' - "frontend/src/**"
- '.github/svelte-check-matcher.json' - ".github/svelte-check-matcher.json"
- 'frontend/package.json' - "frontend/package.json"
- 'frontend/package-lock.json' - "frontend/package-lock.json"
- 'frontend/tsconfig.json' - "frontend/tsconfig.json"
- 'frontend/svelte.config.js' - "frontend/svelte.config.js"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -34,17 +34,15 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 22
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies - name: Install dependencies
run: pnpm --filter pocket-id-frontend install --frozen-lockfile run: pnpm --filter pocket-id-frontend install --frozen-lockfile

View File

@@ -16,8 +16,8 @@ jobs:
actions: write actions: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: "backend/go.mod" go-version-file: "backend/go.mod"
cache-dependency-path: "backend/go.sum" cache-dependency-path: "backend/go.sum"

View File

@@ -15,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Fetch JSON data - name: Fetch JSON data
run: | run: |

View File

@@ -1 +1 @@
1.13.0 1.14.0

View File

@@ -1,3 +1,41 @@
## v1.14.0
### Bug Fixes
- ignore trailing slash in URL ([9f0aa55](https://github.com/pocket-id/pocket-id/commit/9f0aa55be67b7a09810569250563bb388b40590a) by @stonith404)
- use constant time comparisons when validating PKCE challenges ([#1047](https://github.com/pocket-id/pocket-id/pull/1047) by @ItalyPaleAle)
- only animate login background on initial page load ([b356cef](https://github.com/pocket-id/pocket-id/commit/b356cef766697c621157235ae1d2743f3fe6720d) by @stonith404)
- make pkce requirement visible in the oidc form if client is public ([47927d1](https://github.com/pocket-id/pocket-id/commit/47927d157470daa5b5a5b30e61a2ba69110eeff9) by @stonith404)
- prevent page flickering on redirection based on auth state ([10d6403](https://github.com/pocket-id/pocket-id/commit/10d640385ff2078299a07f05e5ca3f0d392eecf7) by @stonith404)
### Features
- add various improvements to the table component ([#961](https://github.com/pocket-id/pocket-id/pull/961) by @stonith404)
- add support for dark mode oidc client icons ([#1039](https://github.com/pocket-id/pocket-id/pull/1039) by @kmendell)
### Other
- add Japanese files ([068fcc6](https://github.com/pocket-id/pocket-id/commit/068fcc65a62c76f55c9636f830fc769bd59220c4) by @kmendell)
- bump sveltekit-superforms from 2.27.1 to 2.27.4 in the npm_and_yarn group across 1 directory ([#1031](https://github.com/pocket-id/pocket-id/pull/1031) by @dependabot[bot])
- update AAGUIDs ([#1041](https://github.com/pocket-id/pocket-id/pull/1041) by @github-actions[bot])
- bump vite from 7.0.7 to 7.0.8 in the npm_and_yarn group across 1 directory ([#1042](https://github.com/pocket-id/pocket-id/pull/1042) by @dependabot[bot])
- upgrade dependencies ([6362ff9](https://github.com/pocket-id/pocket-id/commit/6362ff986124d056cc07d214855f198eab9cb97d) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.13.1...v1.14.0
## v1.13.1
### Bug Fixes
- uploading a client logo with an URL fails ([#1008](https://github.com/pocket-id/pocket-id/pull/1008) by @CzBiX)
- mark any callback url as valid if they contain a wildcard ([#1006](https://github.com/pocket-id/pocket-id/pull/1006) by @stonith404)
### Other
- cleanup root of repo, update workflow actions ([#1003](https://github.com/pocket-id/pocket-id/pull/1003) by @kmendell)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.13.0...v1.13.1
## v1.13.0 ## v1.13.0
### Bug Fixes ### Bug Fixes

View File

@@ -75,6 +75,11 @@ func RegisterFrontend(router *gin.Engine) error {
router.NoRoute(func(c *gin.Context) { router.NoRoute(func(c *gin.Context) {
path := strings.TrimPrefix(c.Request.URL.Path, "/") path := strings.TrimPrefix(c.Request.URL.Path, "/")
if strings.HasSuffix(path, "/") {
c.Redirect(http.StatusMovedPermanently, strings.TrimRight(c.Request.URL.String(), "/"))
return
}
if strings.HasPrefix(path, "api/") { if strings.HasPrefix(path, "api/") {
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
return return
@@ -94,13 +99,9 @@ func RegisterFrontend(router *gin.Engine) error {
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")
c.Header("Cache-Control", "no-store") c.Header("Cache-Control", "no-store")
c.Status(http.StatusOK) c.Status(http.StatusOK)
if err := writeIndexFn(c.Writer, nonce); err != nil {
err = writeIndexFn(c.Writer, nonce)
if err != nil {
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err)) _ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
return
} }
return return
} }

View File

@@ -7,84 +7,87 @@ require (
github.com/cenkalti/backoff/v5 v5.0.3 github.com/cenkalti/backoff/v5 v5.0.3
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.21.3 github.com/emersion/go-smtp v0.24.0
github.com/fxamacker/cbor/v2 v2.9.0 github.com/fxamacker/cbor/v2 v2.9.0
github.com/gin-contrib/slog v1.1.0 github.com/gin-contrib/slog v1.1.0
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.11.0
github.com/glebarez/go-sqlite v1.22.0 github.com/glebarez/go-sqlite v1.22.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-co-op/gocron/v2 v2.16.3 github.com/go-co-op/gocron/v2 v2.17.0
github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-playground/validator/v10 v10.27.0 github.com/go-playground/validator/v10 v10.28.0
github.com/go-webauthn/webauthn v0.11.2 github.com/go-webauthn/webauthn v0.14.0
github.com/golang-migrate/migrate/v4 v4.18.3 github.com/golang-migrate/migrate/v4 v4.19.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-uuid v1.0.3
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/httprc/v3 v3.0.0 github.com/lestrrat-go/httprc/v3 v3.0.1
github.com/lestrrat-go/jwx/v3 v3.0.10 github.com/lestrrat-go/jwx/v3 v3.0.12
github.com/lmittmann/tint v1.1.2 github.com/lmittmann/tint v1.1.2
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mileusna/useragent v1.3.5 github.com/mileusna/useragent v1.3.5
github.com/orandin/slog-gorm v1.4.0 github.com/orandin/slog-gorm v1.4.0
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 github.com/oschwald/maxminddb-golang/v2 v2.0.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 go.opentelemetry.io/contrib/exporters/autoexport v0.63.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/log v0.13.0 go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/metric v1.37.0 go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/log v0.10.0 go.opentelemetry.io/otel/sdk/log v0.14.0
go.opentelemetry.io/otel/sdk/metric v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.37.0 go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.43.0
golang.org/x/image v0.30.0 golang.org/x/image v0.32.0
golang.org/x/sync v0.16.0 golang.org/x/sync v0.17.0
golang.org/x/text v0.28.0 golang.org/x/text v0.30.0
golang.org/x/time v0.12.0 golang.org/x/time v0.14.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.1 gorm.io/gorm v1.31.0
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/disintegration/gift v1.1.2 // indirect github.com/disintegration/gift v1.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-webauthn/x v0.1.23 // indirect github.com/go-webauthn/x v0.1.25 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-github/v39 v39.2.0 // indirect github.com/google/go-github/v39 v39.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect github.com/google/go-tpm v0.9.6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
@@ -93,57 +96,66 @@ require (
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/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/lib/pq v1.10.9 // indirect github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.67.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.18.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.20.0 // indirect go.uber.org/mock v0.6.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/mod v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect golang.org/x/net v0.46.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect golang.org/x/oauth2 v0.32.0 // indirect
google.golang.org/grpc v1.71.0 // indirect golang.org/x/sys v0.37.0 // indirect
google.golang.org/protobuf v1.36.7 // indirect golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.7 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.2 // indirect modernc.org/sqlite v1.39.1 // indirect
) )

View File

@@ -6,10 +6,15 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
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-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
@@ -30,8 +35,11 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs= github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg= github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk= github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@@ -40,6 +48,7 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -48,30 +57,44 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
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/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4= github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4=
github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY= github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
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-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI= github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c= github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -85,18 +108,30 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI= github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E= github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -112,6 +147,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
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=
@@ -119,8 +156,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -137,6 +178,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -177,12 +220,20 @@ 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/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc= github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q= github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw= github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk= github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
@@ -195,6 +246,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -214,6 +267,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -222,6 +277,8 @@ github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsT
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs= github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w= github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
github.com/oschwald/maxminddb-golang/v2 v2.0.0 h1:Gyljxck1kHbBxDgLM++NfDWBqvu1pWWfT8XbosSo0bo=
github.com/oschwald/maxminddb-golang/v2 v2.0.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -230,25 +287,47 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/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=
@@ -261,6 +340,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/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.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
@@ -272,60 +353,116 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g= go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A= go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 h1:HY2hJ7yn3KuEBBBsKxvF3ViSmzLwsgeNvD+0utRMgzc= go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 h1:HY2hJ7yn3KuEBBBsKxvF3ViSmzLwsgeNvD+0utRMgzc=
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0/go.mod h1:H4H7vs8766kwFnOZVEGMJFVF+phpBSmTckvvNRdJeDI= go.opentelemetry.io/contrib/bridges/prometheus v0.59.0/go.mod h1:H4H7vs8766kwFnOZVEGMJFVF+phpBSmTckvvNRdJeDI=
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA= go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA=
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0/go.mod h1:fPl+qlrhRdRntIpPs9JoQ0iBKAsnH5VkgppU1f9kyF4= go.opentelemetry.io/contrib/exporters/autoexport v0.59.0/go.mod h1:fPl+qlrhRdRntIpPs9JoQ0iBKAsnH5VkgppU1f9kyF4=
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 h1:5kSIJ0y8ckZZKoDhZHdVtcyjVi6rXyAwyaR8mp4zLbg=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0/go.mod h1:i+fIMHvcSQtsIY82/xgiVWRklrNt/O6QriHLjzGeY+s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw= go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw=
go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo= go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -336,11 +473,17 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -348,6 +491,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -362,9 +507,13 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -374,6 +523,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -388,6 +539,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -409,8 +562,12 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.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/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -419,17 +576,27 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/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=
@@ -441,18 +608,25 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I= modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY= modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY= modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM= modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -463,6 +637,8 @@ 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.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
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

@@ -45,15 +45,11 @@ func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.Auth
// @Success 200 {object} dto.Paginated[dto.ApiKeyDto] // @Success 200 {object} dto.Paginated[dto.ApiKeyDto]
// @Router /api/api-keys [get] // @Router /api/api-keys [get]
func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) { func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
listRequestOptions := utils.ParseListRequestOptions(ctx)
userID := ctx.GetString("userID") userID := ctx.GetString("userID")
var sortedPaginationRequest utils.SortedPaginationRequest apiKeys, pagination, err := c.apiKeyService.ListApiKeys(ctx.Request.Context(), userID, listRequestOptions)
if err := ctx.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = ctx.Error(err)
return
}
apiKeys, pagination, err := c.apiKeyService.ListApiKeys(ctx.Request.Context(), userID, sortedPaginationRequest)
if err != nil { if err != nil {
_ = ctx.Error(err) _ = ctx.Error(err)
return return

View File

@@ -41,18 +41,12 @@ type AuditLogController struct {
// @Success 200 {object} dto.Paginated[dto.AuditLogDto] // @Success 200 {object} dto.Paginated[dto.AuditLogDto]
// @Router /api/audit-logs [get] // @Router /api/audit-logs [get]
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) { func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
err := c.ShouldBindQuery(&sortedPaginationRequest)
if err != nil {
_ = c.Error(err)
return
}
userID := c.GetString("userID") userID := c.GetString("userID")
// Fetch audit logs for the user // Fetch audit logs for the user
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, sortedPaginationRequest) logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, listRequestOptions)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -86,26 +80,12 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
// @Param pagination[limit] query int false "Number of items per page" default(20) // @Param pagination[limit] query int false "Number of items per page" default(20)
// @Param sort[column] query string false "Column to sort by" // @Param sort[column] query string false "Column to sort by"
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Param filters[userId] query string false "Filter by user ID"
// @Param filters[event] query string false "Filter by event type"
// @Param filters[clientName] query string false "Filter by client name"
// @Param filters[location] query string false "Filter by location type (external or internal)"
// @Success 200 {object} dto.Paginated[dto.AuditLogDto] // @Success 200 {object} dto.Paginated[dto.AuditLogDto]
// @Router /api/audit-logs/all [get] // @Router /api/audit-logs/all [get]
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) { func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err)
return
}
var filters dto.AuditLogFilterDto logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), listRequestOptions)
if err := c.ShouldBindQuery(&filters); err != nil {
_ = c.Error(err)
return
}
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), sortedPaginationRequest, filters)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return

View File

@@ -5,6 +5,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
@@ -357,6 +358,7 @@ func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
clientDto := dto.OidcClientMetaDataDto{} clientDto := dto.OidcClientMetaDataDto{}
err = dto.MapStruct(client, &clientDto) err = dto.MapStruct(client, &clientDto)
if err == nil { if err == nil {
clientDto.HasDarkLogo = client.HasDarkLogo()
c.JSON(http.StatusOK, clientDto) c.JSON(http.StatusOK, clientDto)
return return
} }
@@ -403,13 +405,9 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
// @Router /api/oidc/clients [get] // @Router /api/oidc/clients [get]
func (oc *OidcController) listClientsHandler(c *gin.Context) { func (oc *OidcController) listClientsHandler(c *gin.Context) {
searchTerm := c.Query("search") searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err)
return
}
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, sortedPaginationRequest) clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, listRequestOptions)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -423,6 +421,7 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
_ = c.Error(err) _ = c.Error(err)
return return
} }
clientDto.HasDarkLogo = client.HasDarkLogo()
clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID) clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
@@ -543,10 +542,13 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
// @Produce image/jpeg // @Produce image/jpeg
// @Produce image/svg+xml // @Produce image/svg+xml
// @Param id path string true "Client ID" // @Param id path string true "Client ID"
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
// @Success 200 {file} binary "Logo image" // @Success 200 {file} binary "Logo image"
// @Router /api/oidc/clients/{id}/logo [get] // @Router /api/oidc/clients/{id}/logo [get]
func (oc *OidcController) getClientLogoHandler(c *gin.Context) { func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id")) lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -565,6 +567,7 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
// @Accept multipart/form-data // @Accept multipart/form-data
// @Param id path string true "Client ID" // @Param id path string true "Client ID"
// @Param file formData file true "Logo image file (PNG, JPG, or SVG)" // @Param file formData file true "Logo image file (PNG, JPG, or SVG)"
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
// @Success 204 "No Content" // @Success 204 "No Content"
// @Router /api/oidc/clients/{id}/logo [post] // @Router /api/oidc/clients/{id}/logo [post]
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) { func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
@@ -574,13 +577,16 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
return return
} }
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file) lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file, lightLogo)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// deleteClientLogoHandler godoc // deleteClientLogoHandler godoc
@@ -588,16 +594,26 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
// @Description Delete the logo for an OIDC client // @Description Delete the logo for an OIDC client
// @Tags OIDC // @Tags OIDC
// @Param id path string true "Client ID" // @Param id path string true "Client ID"
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
// @Success 204 "No Content" // @Success 204 "No Content"
// @Router /api/oidc/clients/{id}/logo [delete] // @Router /api/oidc/clients/{id}/logo [delete]
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) { func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
err := oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id")) var err error
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
if lightLogo {
err = oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id"))
} else {
err = oc.oidcService.DeleteClientDarkLogo(c.Request.Context(), c.Param("id"))
}
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// updateAllowedUserGroupsHandler godoc // updateAllowedUserGroupsHandler godoc
@@ -628,6 +644,7 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
_ = c.Error(err) _ = c.Error(err)
return return
} }
oidcClientDto.HasDarkLogo = oidcClient.HasDarkLogo()
c.JSON(http.StatusOK, oidcClientDto) c.JSON(http.StatusOK, oidcClientDto)
} }
@@ -685,12 +702,9 @@ func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
} }
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) { func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err) authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, listRequestOptions)
return
}
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, sortedPaginationRequest)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -741,15 +755,11 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto] // @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
// @Router /api/oidc/users/me/clients [get] // @Router /api/oidc/users/me/clients [get]
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) { func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
listRequestOptions := utils.ParseListRequestOptions(c)
userID := c.GetString("userID") userID := c.GetString("userID")
var sortedPaginationRequest utils.SortedPaginationRequest clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, listRequestOptions)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err)
return
}
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, sortedPaginationRequest)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return

View File

@@ -104,13 +104,9 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
// @Router /api/users [get] // @Router /api/users [get]
func (uc *UserController) listUsersHandler(c *gin.Context) { func (uc *UserController) listUsersHandler(c *gin.Context) {
searchTerm := c.Query("search") searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err)
return
}
users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, sortedPaginationRequest) users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, listRequestOptions)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -574,13 +570,9 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
// @Success 200 {object} dto.Paginated[dto.SignupTokenDto] // @Success 200 {object} dto.Paginated[dto.SignupTokenDto]
// @Router /api/signup-tokens [get] // @Router /api/signup-tokens [get]
func (uc *UserController) listSignupTokensHandler(c *gin.Context) { func (uc *UserController) listSignupTokensHandler(c *gin.Context) {
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err)
return
}
tokens, pagination, err := uc.userService.ListSignupTokens(c.Request.Context(), sortedPaginationRequest) tokens, pagination, err := uc.userService.ListSignupTokens(c.Request.Context(), listRequestOptions)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return

View File

@@ -47,16 +47,10 @@ type UserGroupController struct {
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount] // @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
// @Router /api/user-groups [get] // @Router /api/user-groups [get]
func (ugc *UserGroupController) list(c *gin.Context) { func (ugc *UserGroupController) list(c *gin.Context) {
ctx := c.Request.Context()
searchTerm := c.Query("search") searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest listRequestOptions := utils.ParseListRequestOptions(c)
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
_ = c.Error(err)
return
}
groups, pagination, err := ugc.UserGroupService.List(ctx, searchTerm, sortedPaginationRequest) groups, pagination, err := ugc.UserGroupService.List(c, searchTerm, listRequestOptions)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -70,7 +64,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
_ = c.Error(err) _ = c.Error(err)
return return
} }
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(ctx, group.ID) groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(c.Request.Context(), group.ID)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return

View File

@@ -17,10 +17,3 @@ type AuditLogDto struct {
Username string `json:"username"` Username string `json:"username"`
Data map[string]string `json:"data"` Data map[string]string `json:"data"`
} }
type AuditLogFilterDto struct {
UserID string `form:"filters[userId]"`
Event string `form:"filters[event]"`
ClientName string `form:"filters[clientName]"`
Location string `form:"filters[location]"`
}

View File

@@ -6,6 +6,7 @@ type OidcClientMetaDataDto struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
HasLogo bool `json:"hasLogo"` HasLogo bool `json:"hasLogo"`
HasDarkLogo bool `json:"hasDarkLogo"`
LaunchURL *string `json:"launchURL"` LaunchURL *string `json:"launchURL"`
RequiresReauthentication bool `json:"requiresReauthentication"` RequiresReauthentication bool `json:"requiresReauthentication"`
} }
@@ -39,7 +40,9 @@ type OidcClientUpdateDto struct {
Credentials OidcClientCredentialsDto `json:"credentials"` Credentials OidcClientCredentialsDto `json:"credentials"`
LaunchURL *string `json:"launchURL" binding:"omitempty,url"` LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
HasLogo bool `json:"hasLogo"` HasLogo bool `json:"hasLogo"`
HasDarkLogo bool `json:"hasDarkLogo"`
LogoURL *string `json:"logoUrl"` LogoURL *string `json:"logoUrl"`
DarkLogoURL *string `json:"darkLogoUrl"`
} }
type OidcClientCreateDto struct { type OidcClientCreateDto struct {

View File

@@ -67,14 +67,12 @@ func ValidateClientID(clientID string) bool {
// ValidateCallbackURL validates callback URLs with support for wildcards // ValidateCallbackURL validates callback URLs with support for wildcards
func ValidateCallbackURL(raw string) bool { func ValidateCallbackURL(raw string) bool {
if raw == "*" { // Don't validate if it contains a wildcard
if strings.Contains(raw, "*") {
return true return true
} }
// Replace all '*' with 'x' to check if the rest is still a valid URI u, err := url.Parse(raw)
test := strings.ReplaceAll(raw, "*", "x")
u, err := url.Parse(test)
if err != nil { if err != nil {
return false return false
} }

View File

@@ -3,13 +3,14 @@ package model
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"fmt"
"github.com/pocket-id/pocket-id/backend/internal/utils"
) )
type AuditLog struct { type AuditLog struct {
Base Base
Event AuditLogEvent `sortable:"true"` Event AuditLogEvent `sortable:"true" filterable:"true"`
IpAddress *string `sortable:"true"` IpAddress *string `sortable:"true"`
Country string `sortable:"true"` Country string `sortable:"true"`
City string `sortable:"true"` City string `sortable:"true"`
@@ -17,7 +18,7 @@ type AuditLog struct {
Username string `gorm:"-"` Username string `gorm:"-"`
Data AuditLogData Data AuditLogData
UserID string UserID string `filterable:"true"`
User User User User
} }
@@ -47,14 +48,7 @@ func (e AuditLogEvent) Value() (driver.Value, error) {
} }
func (d *AuditLogData) Scan(value any) error { func (d *AuditLogData) Scan(value any) error {
switch v := value.(type) { return utils.UnmarshalJSONFromDatabase(d, value)
case []byte:
return json.Unmarshal(v, d)
case string:
return json.Unmarshal([]byte(v), d)
default:
return fmt.Errorf("unsupported type: %T", value)
}
} }
func (d AuditLogData) Value() (driver.Value, error) { func (d AuditLogData) Value() (driver.Value, error) {

View File

@@ -3,10 +3,10 @@ package model
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/utils"
) )
type UserAuthorizedOidcClient struct { type UserAuthorizedOidcClient struct {
@@ -52,9 +52,10 @@ type OidcClient struct {
CallbackURLs UrlList CallbackURLs UrlList
LogoutCallbackURLs UrlList LogoutCallbackURLs UrlList
ImageType *string ImageType *string
DarkImageType *string
IsPublic bool IsPublic bool
PkceEnabled bool PkceEnabled bool `filterable:"true"`
RequiresReauthentication bool RequiresReauthentication bool `filterable:"true"`
Credentials OidcClientCredentials Credentials OidcClientCredentials
LaunchURL *string LaunchURL *string
@@ -68,6 +69,10 @@ func (c OidcClient) HasLogo() bool {
return c.ImageType != nil && *c.ImageType != "" return c.ImageType != nil && *c.ImageType != ""
} }
func (c OidcClient) HasDarkLogo() bool {
return c.DarkImageType != nil && *c.DarkImageType != ""
}
type OidcRefreshToken struct { type OidcRefreshToken struct {
Base Base
@@ -116,14 +121,7 @@ func (occ OidcClientCredentials) FederatedIdentityForIssuer(issuer string) (Oidc
} }
func (occ *OidcClientCredentials) Scan(value any) error { func (occ *OidcClientCredentials) Scan(value any) error {
switch v := value.(type) { return utils.UnmarshalJSONFromDatabase(occ, value)
case []byte:
return json.Unmarshal(v, occ)
case string:
return json.Unmarshal([]byte(v), occ)
default:
return fmt.Errorf("unsupported type: %T", value)
}
} }
func (occ OidcClientCredentials) Value() (driver.Value, error) { func (occ OidcClientCredentials) Value() (driver.Value, error) {
@@ -133,14 +131,7 @@ func (occ OidcClientCredentials) Value() (driver.Value, error) {
type UrlList []string //nolint:recvcheck type UrlList []string //nolint:recvcheck
func (cu *UrlList) Scan(value any) error { func (cu *UrlList) Scan(value any) error {
switch v := value.(type) { return utils.UnmarshalJSONFromDatabase(cu, value)
case []byte:
return json.Unmarshal(v, cu)
case string:
return json.Unmarshal([]byte(v), cu)
default:
return fmt.Errorf("unsupported type: %T", value)
}
} }
func (cu UrlList) Value() (driver.Value, error) { func (cu UrlList) Value() (driver.Value, error) {

View File

@@ -18,10 +18,10 @@ type User struct {
FirstName string `sortable:"true"` FirstName string `sortable:"true"`
LastName string `sortable:"true"` LastName string `sortable:"true"`
DisplayName string `sortable:"true"` DisplayName string `sortable:"true"`
IsAdmin bool `sortable:"true"` IsAdmin bool `sortable:"true" filterable:"true"`
Locale *string Locale *string
LdapID *string LdapID *string
Disabled bool `sortable:"true"` Disabled bool `sortable:"true" filterable:"true"`
CustomClaims []CustomClaim CustomClaims []CustomClaim
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"` UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`

View File

@@ -3,11 +3,11 @@ package model
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"fmt"
"time" "time"
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/utils"
) )
type WebauthnSession struct { type WebauthnSession struct {
@@ -16,6 +16,7 @@ type WebauthnSession struct {
Challenge string Challenge string
ExpiresAt datatype.DateTime ExpiresAt datatype.DateTime
UserVerification string UserVerification string
CredentialParams CredentialParameters
} }
type WebauthnCredential struct { type WebauthnCredential struct {
@@ -58,16 +59,20 @@ type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvc
// Scan and Value methods for GORM to handle the custom type // Scan and Value methods for GORM to handle the custom type
func (atl *AuthenticatorTransportList) Scan(value interface{}) error { func (atl *AuthenticatorTransportList) Scan(value interface{}) error {
switch v := value.(type) { return utils.UnmarshalJSONFromDatabase(atl, value)
case []byte:
return json.Unmarshal(v, atl)
case string:
return json.Unmarshal([]byte(v), atl)
default:
return fmt.Errorf("unsupported type: %T", value)
}
} }
func (atl AuthenticatorTransportList) Value() (driver.Value, error) { func (atl AuthenticatorTransportList) Value() (driver.Value, error) {
return json.Marshal(atl) return json.Marshal(atl)
} }
type CredentialParameters []protocol.CredentialParameter //nolint:recvcheck
// Scan and Value methods for GORM to handle the custom type
func (cp *CredentialParameters) Scan(value interface{}) error {
return utils.UnmarshalJSONFromDatabase(cp, value)
}
func (cp CredentialParameters) Value() (driver.Value, error) {
return json.Marshal(cp)
}

View File

@@ -25,14 +25,14 @@ func NewApiKeyService(db *gorm.DB, emailService *EmailService) *ApiKeyService {
return &ApiKeyService{db: db, emailService: emailService} return &ApiKeyService{db: db, emailService: emailService}
} }
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.ApiKey, utils.PaginationResponse, error) { func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.ApiKey, utils.PaginationResponse, error) {
query := s.db. query := s.db.
WithContext(ctx). WithContext(ctx).
Where("user_id = ?", userID). Where("user_id = ?", userID).
Model(&model.ApiKey{}) Model(&model.ApiKey{})
var apiKeys []model.ApiKey var apiKeys []model.ApiKey
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &apiKeys) pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &apiKeys)
if err != nil { if err != nil {
return nil, utils.PaginationResponse{}, err return nil, utils.PaginationResponse{}, err
} }

View File

@@ -6,7 +6,6 @@ import (
"log/slog" "log/slog"
userAgentParser "github.com/mileusna/useragent" userAgentParser "github.com/mileusna/useragent"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/internal/utils/email" "github.com/pocket-id/pocket-id/backend/internal/utils/email"
@@ -136,14 +135,14 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
} }
// ListAuditLogsForUser retrieves all audit logs for a given user ID // ListAuditLogsForUser retrieves all audit logs for a given user ID
func (s *AuditLogService) ListAuditLogsForUser(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.AuditLog, utils.PaginationResponse, error) { func (s *AuditLogService) ListAuditLogsForUser(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.AuditLog, utils.PaginationResponse, error) {
var logs []model.AuditLog var logs []model.AuditLog
query := s.db. query := s.db.
WithContext(ctx). WithContext(ctx).
Model(&model.AuditLog{}). Model(&model.AuditLog{}).
Where("user_id = ?", userID) Where("user_id = ?", userID)
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs) pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &logs)
return logs, pagination, err return logs, pagination, err
} }
@@ -152,7 +151,7 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
return ua.Name + " on " + ua.OS + " " + ua.OSVersion return ua.Name + " on " + ua.OS + " " + ua.OSVersion
} }
func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPaginationRequest utils.SortedPaginationRequest, filters dto.AuditLogFilterDto) ([]model.AuditLog, utils.PaginationResponse, error) { func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.AuditLog, utils.PaginationResponse, error) {
var logs []model.AuditLog var logs []model.AuditLog
query := s.db. query := s.db.
@@ -160,33 +159,36 @@ func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPagination
Preload("User"). Preload("User").
Model(&model.AuditLog{}) Model(&model.AuditLog{})
if filters.UserID != "" { if clientName, ok := listRequestOptions.Filters["clientName"]; ok {
query = query.Where("user_id = ?", filters.UserID)
}
if filters.Event != "" {
query = query.Where("event = ?", filters.Event)
}
if filters.ClientName != "" {
dialect := s.db.Name() dialect := s.db.Name()
switch dialect { switch dialect {
case "sqlite": case "sqlite":
query = query.Where("json_extract(data, '$.clientName') = ?", filters.ClientName) query = query.Where("json_extract(data, '$.clientName') IN ?", clientName)
case "postgres": case "postgres":
query = query.Where("data->>'clientName' = ?", filters.ClientName) query = query.Where("data->>'clientName' IN ?", clientName)
default: default:
return nil, utils.PaginationResponse{}, fmt.Errorf("unsupported database dialect: %s", dialect) return nil, utils.PaginationResponse{}, fmt.Errorf("unsupported database dialect: %s", dialect)
} }
} }
if filters.Location != "" {
switch filters.Location { if locations, ok := listRequestOptions.Filters["location"]; ok {
case "external": mapped := make([]string, 0, len(locations))
query = query.Where("country != 'Internal Network'") for _, v := range locations {
case "internal": if s, ok := v.(string); ok {
query = query.Where("country = 'Internal Network'") switch s {
case "internal":
mapped = append(mapped, "Internal Network")
case "external":
mapped = append(mapped, "External Network")
}
}
}
if len(mapped) > 0 {
query = query.Where("country IN ?", mapped)
} }
} }
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs) pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &logs)
if err != nil { if err != nil {
return nil, pagination, err return nil, pagination, err
} }

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"crypto/subtle"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@@ -15,6 +16,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
@@ -393,7 +395,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, inpu
// If the client is public or PKCE is enabled, the code verifier must match the code challenge // If the client is public or PKCE is enabled, the code verifier must match the code challenge
if client.IsPublic || client.PkceEnabled { if client.IsPublic || client.PkceEnabled {
if !s.validateCodeVerifier(input.CodeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) { if !validateCodeVerifier(input.CodeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
return CreatedTokens{}, &common.OidcInvalidCodeVerifierError{} return CreatedTokens{}, &common.OidcInvalidCodeVerifierError{}
} }
} }
@@ -691,7 +693,7 @@ func (s *OidcService) getClientInternal(ctx context.Context, clientID string, tx
return client, nil return client, nil
} }
func (s *OidcService) ListClients(ctx context.Context, name string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.OidcClient, utils.PaginationResponse, error) { func (s *OidcService) ListClients(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) ([]model.OidcClient, utils.PaginationResponse, error) {
var clients []model.OidcClient var clients []model.OidcClient
query := s.db. query := s.db.
@@ -704,17 +706,17 @@ func (s *OidcService) ListClients(ctx context.Context, name string, sortedPagina
} }
// As allowedUserGroupsCount is not a column, we need to manually sort it // As allowedUserGroupsCount is not a column, we need to manually sort it
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) { if listRequestOptions.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)"). query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id"). Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
Group("oidc_clients.id"). Group("oidc_clients.id").
Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + sortedPaginationRequest.Sort.Direction) Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + listRequestOptions.Sort.Direction)
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &clients) response, err := utils.Paginate(listRequestOptions.Pagination.Page, listRequestOptions.Pagination.Limit, query, &clients)
return clients, response, err return clients, response, err
} }
response, err := utils.PaginateAndSort(sortedPaginationRequest, query, &clients) response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
return clients, response, err return clients, response, err
} }
@@ -744,12 +746,19 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
} }
if input.LogoURL != nil { if input.LogoURL != nil {
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL) err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL, true)
if err != nil { if err != nil {
return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err) return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
} }
} }
if input.DarkLogoURL != nil {
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.DarkLogoURL, false)
if err != nil {
return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
}
}
err = tx.Commit().Error err = tx.Commit().Error
if err != nil { if err != nil {
return model.OidcClient{}, err return model.OidcClient{}, err
@@ -776,12 +785,19 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
} }
if input.LogoURL != nil { if input.LogoURL != nil {
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL) err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL, true)
if err != nil { if err != nil {
return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err) return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
} }
} }
if input.DarkLogoURL != nil {
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.DarkLogoURL, false)
if err != nil {
return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
}
}
if err := tx.Commit().Error; err != nil { if err := tx.Commit().Error; err != nil {
return model.OidcClient{}, err return model.OidcClient{}, err
} }
@@ -868,7 +884,7 @@ func (s *OidcService) CreateClientSecret(ctx context.Context, clientID string) (
return clientSecret, nil return clientSecret, nil
} }
func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (string, string, error) { func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light bool) (string, string, error) {
var client model.OidcClient var client model.OidcClient
err := s.db. err := s.db.
WithContext(ctx). WithContext(ctx).
@@ -878,23 +894,38 @@ func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (strin
return "", "", err return "", "", err
} }
if client.ImageType == nil { var imagePath, mimeType string
switch {
case !light && client.DarkImageType != nil:
// Dark logo if requested and exists
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + *client.DarkImageType
mimeType = utils.GetImageMimeType(*client.DarkImageType)
case client.ImageType != nil:
// Light logo if requested or no dark logo is available
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType
mimeType = utils.GetImageMimeType(*client.ImageType)
default:
return "", "", errors.New("image not found") return "", "", errors.New("image not found")
} }
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType
mimeType := utils.GetImageMimeType(*client.ImageType)
return imagePath, mimeType, nil return imagePath, mimeType, nil
} }
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader) error { func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader, light bool) error {
fileType := strings.ToLower(utils.GetFileExtension(file.Filename)) fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" { if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
return &common.FileTypeNotSupportedError{} return &common.FileTypeNotSupportedError{}
} }
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + "." + fileType var darkSuffix string
if !light {
darkSuffix = "-dark"
}
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + darkSuffix + "." + fileType
err := utils.SaveFile(file, imagePath) err := utils.SaveFile(file, imagePath)
if err != nil { if err != nil {
return err return err
@@ -902,7 +933,7 @@ func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, fil
tx := s.db.Begin() tx := s.db.Begin()
err = s.updateClientLogoType(ctx, tx, clientID, fileType) err = s.updateClientLogoType(ctx, tx, clientID, fileType, light)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return err return err
@@ -954,6 +985,49 @@ func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) err
return nil return nil
} }
func (s *OidcService) DeleteClientDarkLogo(ctx context.Context, clientID string) error {
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
var client model.OidcClient
err := tx.
WithContext(ctx).
First(&client, "id = ?", clientID).
Error
if err != nil {
return err
}
if client.DarkImageType == nil {
return errors.New("image not found")
}
oldImageType := *client.DarkImageType
client.DarkImageType = nil
err = tx.
WithContext(ctx).
Save(&client).
Error
if err != nil {
return err
}
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + oldImageType
if err := os.Remove(imagePath); err != nil {
return err
}
err = tx.Commit().Error
if err != nil {
return err
}
return nil
}
func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) { func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) {
tx := s.db.Begin() tx := s.db.Begin()
defer func() { defer func() {
@@ -1082,13 +1156,20 @@ func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID stri
return randomString, nil return randomString, nil
} }
func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool { func validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
if codeVerifier == "" || codeChallenge == "" { if codeVerifier == "" || codeChallenge == "" {
return false return false
} }
if !codeChallengeMethodSha256 { if !codeChallengeMethodSha256 {
return codeVerifier == codeChallenge return subtle.ConstantTimeCompare([]byte(codeVerifier), []byte(codeChallenge)) == 1
}
// Base64 URL decode the challenge
// If it's not valid base64url, fail the operation
codeChallengeBytes, err := base64.RawURLEncoding.DecodeString(codeChallenge)
if err != nil {
return false
} }
// Compute SHA-256 hash of the codeVerifier // Compute SHA-256 hash of the codeVerifier
@@ -1096,10 +1177,7 @@ func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, c
h.Write([]byte(codeVerifier)) h.Write([]byte(codeVerifier))
codeVerifierHash := h.Sum(nil) codeVerifierHash := h.Sum(nil)
// Base64 URL encode the verifier hash return subtle.ConstantTimeCompare(codeVerifierHash, codeChallengeBytes) == 1
encodedVerifierHash := base64.RawURLEncoding.EncodeToString(codeVerifierHash)
return encodedVerifierHash == codeChallenge
} }
func (s *OidcService) getCallbackURL(client *model.OidcClient, inputCallbackURL string, tx *gorm.DB, ctx context.Context) (callbackURL string, err error) { func (s *OidcService) getCallbackURL(client *model.OidcClient, inputCallbackURL string, tx *gorm.DB, ctx context.Context) (callbackURL string, err error) {
@@ -1323,9 +1401,10 @@ func (s *OidcService) GetDeviceCodeInfo(ctx context.Context, userCode string, us
return &dto.DeviceCodeInfoDto{ return &dto.DeviceCodeInfoDto{
Client: dto.OidcClientMetaDataDto{ Client: dto.OidcClientMetaDataDto{
ID: deviceAuth.Client.ID, ID: deviceAuth.Client.ID,
Name: deviceAuth.Client.Name, Name: deviceAuth.Client.Name,
HasLogo: deviceAuth.Client.HasLogo(), HasLogo: deviceAuth.Client.HasLogo(),
HasDarkLogo: deviceAuth.Client.HasDarkLogo(),
}, },
Scope: deviceAuth.Scope, Scope: deviceAuth.Scope,
AuthorizationRequired: !hasAuthorizedClient, AuthorizationRequired: !hasAuthorizedClient,
@@ -1349,7 +1428,7 @@ func (s *OidcService) GetAllowedGroupsCountOfClient(ctx context.Context, id stri
return count, nil return count, nil
} }
func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) { func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {
query := s.db. query := s.db.
WithContext(ctx). WithContext(ctx).
@@ -1358,7 +1437,7 @@ func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string,
Where("user_id = ?", userID) Where("user_id = ?", userID)
var authorizedClients []model.UserAuthorizedOidcClient var authorizedClients []model.UserAuthorizedOidcClient
response, err := utils.PaginateAndSort(sortedPaginationRequest, query, &authorizedClients) response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &authorizedClients)
return authorizedClients, response, err return authorizedClients, response, err
} }
@@ -1391,7 +1470,7 @@ func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string,
return nil return nil
} }
func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) { func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
tx := s.db.Begin() tx := s.db.Begin()
defer func() { defer func() {
tx.Rollback() tx.Rollback()
@@ -1438,13 +1517,13 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
// Handle custom sorting for lastUsedAt column // Handle custom sorting for lastUsedAt column
var response utils.PaginationResponse var response utils.PaginationResponse
if sortedPaginationRequest.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) { if listRequestOptions.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
query = query. query = query.
Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID). Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction + " NULLS LAST") Order("user_authorized_oidc_clients.last_used_at " + listRequestOptions.Sort.Direction + " NULLS LAST")
} }
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &clients) response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
if err != nil { if err != nil {
return nil, utils.PaginationResponse{}, err return nil, utils.PaginationResponse{}, err
} }
@@ -1457,10 +1536,11 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
} }
dtos[i] = dto.AccessibleOidcClientDto{ dtos[i] = dto.AccessibleOidcClientDto{
OidcClientMetaDataDto: dto.OidcClientMetaDataDto{ OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
ID: client.ID, ID: client.ID,
Name: client.Name, Name: client.Name,
LaunchURL: client.LaunchURL, LaunchURL: client.LaunchURL,
HasLogo: client.HasLogo(), HasLogo: client.HasLogo(),
HasDarkLogo: client.HasDarkLogo(),
}, },
LastUsedAt: lastUsedAt, LastUsedAt: lastUsedAt,
} }
@@ -1882,7 +1962,7 @@ func (s *OidcService) IsClientAccessibleToUser(ctx context.Context, clientID str
return s.IsUserGroupAllowedToAuthorize(user, client), nil return s.IsUserGroupAllowedToAuthorize(user, client), nil
} }
func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *gorm.DB, clientID string, raw string) error { func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *gorm.DB, clientID string, raw string, light bool) error {
u, err := url.Parse(raw) u, err := url.Parse(raw)
if err != nil { if err != nil {
return err return err
@@ -1937,36 +2017,57 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *
return &common.FileTypeNotSupportedError{} return &common.FileTypeNotSupportedError{}
} }
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + "." + ext folderPath := filepath.Join(common.EnvConfig.UploadPath, "oidc-client-images")
err = os.MkdirAll(imagePath, os.ModePerm) err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil { if err != nil {
return err return err
} }
var darkSuffix string
if !light {
darkSuffix = "-dark"
}
imagePath := filepath.Join(folderPath, clientID+darkSuffix+"."+ext)
err = utils.SaveFileStream(io.LimitReader(resp.Body, maxLogoSize+1), imagePath) err = utils.SaveFileStream(io.LimitReader(resp.Body, maxLogoSize+1), imagePath)
if err != nil { if err != nil {
return err return err
} }
if err := s.updateClientLogoType(ctx, tx, clientID, ext); err != nil { if err := s.updateClientLogoType(ctx, tx, clientID, ext, light); err != nil {
return err return err
} }
return nil return nil
} }
func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, clientID, ext string) error { func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, clientID, ext string, light bool) error {
uploadsDir := common.EnvConfig.UploadPath + "/oidc-client-images" uploadsDir := common.EnvConfig.UploadPath + "/oidc-client-images"
var darkSuffix string
if !light {
darkSuffix = "-dark"
}
var client model.OidcClient var client model.OidcClient
if err := tx.WithContext(ctx).First(&client, "id = ?", clientID).Error; err != nil { if err := tx.WithContext(ctx).First(&client, "id = ?", clientID).Error; err != nil {
return err return err
} }
if client.ImageType != nil && *client.ImageType != ext { if client.ImageType != nil && *client.ImageType != ext {
old := fmt.Sprintf("%s/%s.%s", uploadsDir, client.ID, *client.ImageType) old := fmt.Sprintf("%s/%s%s.%s", uploadsDir, client.ID, darkSuffix, *client.ImageType)
_ = os.Remove(old) _ = os.Remove(old)
} }
client.ImageType = &ext
return tx.WithContext(ctx).Save(&client).Error
var column string
if light {
column = "image_type"
} else {
column = "dark_image_type"
}
return tx.WithContext(ctx).
Model(&model.OidcClient{}).
Where("id = ?", clientID).
Update(column, ext).
Error
} }

View File

@@ -5,6 +5,8 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json" "encoding/json"
"net/http" "net/http"
"testing" "testing"
@@ -510,3 +512,28 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
}) })
}) })
} }
func TestValidateCodeVerifier_Plain(t *testing.T) {
require.False(t, validateCodeVerifier("", "", false))
require.False(t, validateCodeVerifier("", "", true))
t.Run("plain", func(t *testing.T) {
require.False(t, validateCodeVerifier("", "challenge", false))
require.False(t, validateCodeVerifier("verifier", "", false))
require.True(t, validateCodeVerifier("plainVerifier", "plainVerifier", false))
require.False(t, validateCodeVerifier("plainVerifier", "otherVerifier", false))
})
t.Run("SHA 256", func(t *testing.T) {
codeVerifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
hash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
require.True(t, validateCodeVerifier(codeVerifier, codeChallenge, true))
require.False(t, validateCodeVerifier("wrongVerifier", codeChallenge, true))
require.False(t, validateCodeVerifier(codeVerifier, "!", true))
// Invalid base64
require.False(t, validateCodeVerifier("NOT!VALID", codeChallenge, true))
})
}

View File

@@ -21,7 +21,7 @@ func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService) *UserG
return &UserGroupService{db: db, appConfigService: appConfigService} return &UserGroupService{db: db, appConfigService: appConfigService}
} }
func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) { func (s *UserGroupService) List(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
query := s.db. query := s.db.
WithContext(ctx). WithContext(ctx).
Preload("CustomClaims"). Preload("CustomClaims").
@@ -32,17 +32,14 @@ func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginati
} }
// As userCount is not a column we need to manually sort it // As userCount is not a column we need to manually sort it
if sortedPaginationRequest.Sort.Column == "userCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) { if listRequestOptions.Sort.Column == "userCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)"). query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id"). Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
Group("user_groups.id"). Group("user_groups.id").
Order("COUNT(user_groups_users.user_id) " + sortedPaginationRequest.Sort.Direction) Order("COUNT(user_groups_users.user_id) " + listRequestOptions.Sort.Direction)
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &groups)
return groups, response, err
} }
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &groups) response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &groups)
return groups, response, err return groups, response, err
} }

View File

@@ -46,7 +46,7 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
} }
} }
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) { func (s *UserService) ListUsers(ctx context.Context, searchTerm string, listRequestOptions utils.ListRequestOptions) ([]model.User, utils.PaginationResponse, error) {
var users []model.User var users []model.User
query := s.db.WithContext(ctx). query := s.db.WithContext(ctx).
Model(&model.User{}). Model(&model.User{}).
@@ -60,7 +60,7 @@ func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPa
searchPattern, searchPattern, searchPattern, searchPattern) searchPattern, searchPattern, searchPattern, searchPattern)
} }
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users) pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &users)
return users, pagination, err return users, pagination, err
} }
@@ -794,11 +794,11 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
return user, accessToken, nil return user, accessToken, nil
} }
func (s *UserService) ListSignupTokens(ctx context.Context, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.SignupToken, utils.PaginationResponse, error) { func (s *UserService) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.SignupToken, utils.PaginationResponse, error) {
var tokens []model.SignupToken var tokens []model.SignupToken
query := s.db.WithContext(ctx).Model(&model.SignupToken{}) query := s.db.WithContext(ctx).Model(&model.SignupToken{})
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &tokens) pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &tokens)
return tokens, pagination, err return tokens, pagination, err
} }

View File

@@ -89,6 +89,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
sessionToStore := &model.WebauthnSession{ sessionToStore := &model.WebauthnSession{
ExpiresAt: datatype.DateTime(session.Expires), ExpiresAt: datatype.DateTime(session.Expires),
Challenge: session.Challenge, Challenge: session.Challenge,
CredentialParams: session.CredParams,
UserVerification: string(session.UserVerification), UserVerification: string(session.UserVerification),
} }
@@ -130,9 +131,10 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
} }
session := webauthn.SessionData{ session := webauthn.SessionData{
Challenge: storedSession.Challenge, Challenge: storedSession.Challenge,
Expires: storedSession.ExpiresAt.ToTime(), Expires: storedSession.ExpiresAt.ToTime(),
UserID: []byte(userID), CredParams: storedSession.CredentialParams,
UserID: []byte(userID),
} }
var user model.User var user model.User

View File

@@ -3,6 +3,7 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"time" "time"
) )
@@ -40,3 +41,14 @@ func (d *JSONDuration) UnmarshalJSON(b []byte) error {
return errors.New("invalid duration") return errors.New("invalid duration")
} }
} }
func UnmarshalJSONFromDatabase(data interface{}, value any) error {
switch v := value.(type) {
case []byte:
return json.Unmarshal(v, data)
case string:
return json.Unmarshal([]byte(v), data)
default:
return fmt.Errorf("unsupported type: %T", value)
}
}

View File

@@ -0,0 +1,205 @@
package utils
import (
"reflect"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PaginationResponse struct {
TotalPages int64 `json:"totalPages"`
TotalItems int64 `json:"totalItems"`
CurrentPage int `json:"currentPage"`
ItemsPerPage int `json:"itemsPerPage"`
}
type ListRequestOptions struct {
Pagination struct {
Page int `form:"pagination[page]"`
Limit int `form:"pagination[limit]"`
} `form:"pagination"`
Sort struct {
Column string `form:"sort[column]"`
Direction string `form:"sort[direction]"`
} `form:"sort"`
Filters map[string][]any
}
type FieldMeta struct {
ColumnName string
IsSortable bool
IsFilterable bool
}
func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOptions) {
if err := ctx.ShouldBindQuery(&listRequestOptions); err != nil {
return listRequestOptions
}
listRequestOptions.Filters = parseNestedFilters(ctx)
return listRequestOptions
}
func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result interface{}) (PaginationResponse, error) {
meta := extractModelMetadata(result)
query = applyFilters(params.Filters, query, meta)
query = applySorting(params.Sort.Column, params.Sort.Direction, query, meta)
return Paginate(params.Pagination.Page, params.Pagination.Limit, query, result)
}
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
} else if pageSize > 100 {
pageSize = 100
}
var totalItems int64
if err := query.Count(&totalItems).Error; err != nil {
return PaginationResponse{}, err
}
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
if totalItems == 0 {
totalPages = 1
}
if int64(page) > totalPages {
page = int(totalPages)
}
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
return PaginationResponse{}, err
}
return PaginationResponse{
TotalPages: totalPages,
TotalItems: totalItems,
CurrentPage: page,
ItemsPerPage: pageSize,
}, nil
}
func NormalizeSortDirection(direction string) string {
d := strings.ToLower(strings.TrimSpace(direction))
if d != "asc" && d != "desc" {
return "asc"
}
return d
}
func IsValidSortDirection(direction string) bool {
d := strings.ToLower(strings.TrimSpace(direction))
return d == "asc" || d == "desc"
}
// parseNestedFilters handles ?filters[field][0]=val1&filters[field][1]=val2
func parseNestedFilters(ctx *gin.Context) map[string][]any {
result := make(map[string][]any)
query := ctx.Request.URL.Query()
for key, values := range query {
if !strings.HasPrefix(key, "filters[") {
continue
}
// Keys can be "filters[field]" or "filters[field][0]"
raw := strings.TrimPrefix(key, "filters[")
// Take everything up to the first closing bracket
if idx := strings.IndexByte(raw, ']'); idx != -1 {
field := raw[:idx]
for _, v := range values {
result[field] = append(result[field], ConvertStringToType(v))
}
}
}
return result
}
// applyFilters applies filtering to the GORM query based on the provided filters
func applyFilters(filters map[string][]any, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
for key, values := range filters {
if key == "" || len(values) == 0 {
continue
}
fieldName := CapitalizeFirstLetter(key)
fieldMeta, ok := meta[fieldName]
if !ok || !fieldMeta.IsFilterable {
continue
}
query = query.Where(fieldMeta.ColumnName+" IN ?", values)
}
return query
}
// applySorting applies sorting to the GORM query based on the provided column and direction
func applySorting(sortColumn string, sortDirection string, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
fieldName := CapitalizeFirstLetter(sortColumn)
fieldMeta, ok := meta[fieldName]
if !ok || !fieldMeta.IsSortable {
return query
}
sortDirection = NormalizeSortDirection(sortDirection)
query = query.Clauses(clause.OrderBy{
Columns: []clause.OrderByColumn{
{Column: clause.Column{Name: fieldMeta.ColumnName}, Desc: sortDirection == "desc"},
},
})
return query
}
// extractModelMetadata extracts FieldMeta from the model struct using reflection
func extractModelMetadata(model interface{}) map[string]FieldMeta {
meta := make(map[string]FieldMeta)
// Unwrap pointers and slices to get the element struct type
t := reflect.TypeOf(model)
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
t = t.Elem()
if t == nil {
return meta
}
}
// recursive parser that merges fields from embedded structs
var parseStruct func(reflect.Type)
parseStruct = func(st reflect.Type) {
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
ft := field.Type
// If the field is an embedded/anonymous struct, recurse into it
if field.Anonymous && ft.Kind() == reflect.Struct {
parseStruct(ft)
continue
}
// Normal field: record metadata
name := field.Name
meta[name] = FieldMeta{
ColumnName: CamelCaseToSnakeCase(name),
IsSortable: field.Tag.Get("sortable") == "true",
IsFilterable: field.Tag.Get("filterable") == "true",
}
}
}
parseStruct(t)
return meta
}

View File

@@ -1,99 +0,0 @@
package utils
import (
"reflect"
"strconv"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PaginationResponse struct {
TotalPages int64 `json:"totalPages"`
TotalItems int64 `json:"totalItems"`
CurrentPage int `json:"currentPage"`
ItemsPerPage int `json:"itemsPerPage"`
}
type SortedPaginationRequest struct {
Pagination struct {
Page int `form:"pagination[page]"`
Limit int `form:"pagination[limit]"`
} `form:"pagination"`
Sort struct {
Column string `form:"sort[column]"`
Direction string `form:"sort[direction]"`
} `form:"sort"`
}
func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gorm.DB, result interface{}) (PaginationResponse, error) {
pagination := sortedPaginationRequest.Pagination
sort := sortedPaginationRequest.Sort
capitalizedSortColumn := CapitalizeFirstLetter(sort.Column)
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
sort.Direction = NormalizeSortDirection(sort.Direction)
if sortFieldFound && isSortable {
columnName := CamelCaseToSnakeCase(sort.Column)
query = query.Clauses(clause.OrderBy{
Columns: []clause.OrderByColumn{
{Column: clause.Column{Name: columnName}, Desc: sort.Direction == "desc"},
},
})
}
return Paginate(pagination.Page, pagination.Limit, query, result)
}
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
} else if pageSize > 100 {
pageSize = 100
}
offset := (page - 1) * pageSize
var totalItems int64
if err := query.Count(&totalItems).Error; err != nil {
return PaginationResponse{}, err
}
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
return PaginationResponse{}, err
}
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
if totalItems == 0 {
totalPages = 1
}
return PaginationResponse{
TotalPages: totalPages,
TotalItems: totalItems,
CurrentPage: page,
ItemsPerPage: pageSize,
}, nil
}
func NormalizeSortDirection(direction string) string {
d := strings.ToLower(strings.TrimSpace(direction))
if d != "asc" && d != "desc" {
return "asc"
}
return d
}
func IsValidSortDirection(direction string) bool {
d := strings.ToLower(strings.TrimSpace(direction))
return d == "asc" || d == "desc"
}

View File

@@ -81,26 +81,21 @@ func CapitalizeFirstLetter(str string) string {
return result.String() return result.String()
} }
func CamelCaseToSnakeCase(str string) string { var (
result := strings.Builder{} reAcronymBoundary = regexp.MustCompile(`([A-Z]+)([A-Z][a-z])`) // ABCd -> AB_Cd
result.Grow(int(float32(len(str)) * 1.1)) reLowerToUpper = regexp.MustCompile(`([a-z0-9])([A-Z])`) // aB -> a_B
for i, r := range str { )
if unicode.IsUpper(r) && i > 0 {
result.WriteByte('_') func CamelCaseToSnakeCase(s string) string {
} s = reAcronymBoundary.ReplaceAllString(s, "${1}_${2}")
result.WriteRune(unicode.ToLower(r)) s = reLowerToUpper.ReplaceAllString(s, "${1}_${2}")
} return strings.ToLower(s)
return result.String()
} }
var camelCaseToScreamingSnakeCaseRe = regexp.MustCompile(`([a-z0-9])([A-Z])`)
func CamelCaseToScreamingSnakeCase(s string) string { func CamelCaseToScreamingSnakeCase(s string) string {
// Insert underscores before uppercase letters (except the first one) s = reAcronymBoundary.ReplaceAllString(s, "${1}_${2}")
snake := camelCaseToScreamingSnakeCaseRe.ReplaceAllString(s, `${1}_${2}`) s = reLowerToUpper.ReplaceAllString(s, "${1}_${2}")
return strings.ToUpper(s)
// Convert to uppercase
return strings.ToUpper(snake)
} }
// GetFirstCharacter returns the first non-whitespace character of the string, correctly handling Unicode // GetFirstCharacter returns the first non-whitespace character of the string, correctly handling Unicode

View File

@@ -86,9 +86,9 @@ func TestCamelCaseToSnakeCase(t *testing.T) {
{"simple camelCase", "camelCase", "camel_case"}, {"simple camelCase", "camelCase", "camel_case"},
{"PascalCase", "PascalCase", "pascal_case"}, {"PascalCase", "PascalCase", "pascal_case"},
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "multiple_words_in_camel_case"}, {"multipleWordsInCamelCase", "multipleWordsInCamelCase", "multiple_words_in_camel_case"},
{"consecutive uppercase", "HTTPRequest", "h_t_t_p_request"}, {"consecutive uppercase", "HTTPRequest", "http_request"},
{"single lowercase word", "word", "word"}, {"single lowercase word", "word", "word"},
{"single uppercase word", "WORD", "w_o_r_d"}, {"single uppercase word", "WORD", "word"},
{"with numbers", "camel123Case", "camel123_case"}, {"with numbers", "camel123Case", "camel123_case"},
{"with numbers in middle", "model2Name", "model2_name"}, {"with numbers in middle", "model2Name", "model2_name"},
{"mixed case", "iPhone6sPlus", "i_phone6s_plus"}, {"mixed case", "iPhone6sPlus", "i_phone6s_plus"},
@@ -104,6 +104,34 @@ func TestCamelCaseToSnakeCase(t *testing.T) {
} }
} }
func TestCamelCaseToScreamingSnakeCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty string", "", ""},
{"simple camelCase", "camelCase", "CAMEL_CASE"},
{"PascalCase", "PascalCase", "PASCAL_CASE"},
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "MULTIPLE_WORDS_IN_CAMEL_CASE"},
{"consecutive uppercase", "HTTPRequest", "HTTP_REQUEST"},
{"single lowercase word", "word", "WORD"},
{"single uppercase word", "WORD", "WORD"},
{"with numbers", "camel123Case", "CAMEL123_CASE"},
{"with numbers in middle", "model2Name", "MODEL2_NAME"},
{"mixed case", "iPhone6sPlus", "I_PHONE6S_PLUS"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CamelCaseToScreamingSnakeCase(tt.input)
if result != tt.expected {
t.Errorf("CamelCaseToScreamingSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestGetFirstCharacter(t *testing.T) { func TestGetFirstCharacter(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@@ -0,0 +1,35 @@
package utils
import (
"strconv"
"strings"
)
// ConvertStringToType attempts to convert a string to bool, int, or float.
func ConvertStringToType(value string) any {
v := strings.TrimSpace(value)
if v == "" {
return v
}
// Try bool
if v == "true" {
return true
}
if v == "false" {
return false
}
// Try int
if i, err := strconv.Atoi(v); err == nil {
return i
}
// Try float
if f, err := strconv.ParseFloat(v, 64); err == nil {
return f
}
// Default: string
return v
}

View File

@@ -0,0 +1,37 @@
package utils
import (
"testing"
)
func TestConvertStringToType(t *testing.T) {
tests := []struct {
input string
expected any
}{
{"true", true},
{"false", false},
{" true ", true},
{" false ", false},
{"42", 42},
{" 42 ", 42},
{"3.14", 3.14},
{" 3.14 ", 3.14},
{"hello", "hello"},
{" hello ", "hello"},
{"", ""},
{" ", ""},
}
for _, tt := range tests {
result := ConvertStringToType(tt.input)
if result != tt.expected {
if f, ok := tt.expected.(float64); ok {
if rf, ok := result.(float64); ok && rf == f {
continue
}
}
t.Errorf("ConvertStringToType(%q) = %#v (type %T), want %#v (type %T)", tt.input, result, result, tt.expected, tt.expected)
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
ALTER TABLE oidc_clients DROP COLUMN dark_image_type;

View File

@@ -0,0 +1 @@
ALTER TABLE oidc_clients ADD COLUMN dark_image_type TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE webauthn_sessions DROP COLUMN credential_params;

View File

@@ -0,0 +1 @@
ALTER TABLE webauthn_sessions ADD COLUMN credential_params JSONB NOT NULL DEFAULT '[]';

View File

@@ -0,0 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN dark_image_type;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients ADD COLUMN dark_image_type TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE webauthn_sessions DROP COLUMN credential_params;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE webauthn_sessions ADD COLUMN credential_params TEXT NOT NULL DEFAULT '[]';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -10,16 +10,16 @@
}, },
"dependencies": { "dependencies": {
"@react-email/components": "0.1.1", "@react-email/components": "0.1.1",
"react": "^19.0.0", "react": "^19.2.0",
"react-dom": "^19.0.0", "react-dom": "^19.2.0",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
"@react-email/preview-server": "4.2.8", "@react-email/preview-server": "4.2.8",
"@types/node": "^24.0.10", "@types/node": "^24.9.1",
"@types/react": "^19.0.1", "@types/react": "^19.2.2",
"@types/react-dom": "^19.0.1", "@types/react-dom": "^19.2.2",
"react-email": "4.2.8", "react-email": "4.2.8",
"tsx": "^4.0.0" "tsx": "^4.20.6"
} }
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Vložte přímou URL adresu obrázku (svg, png, webp). Ikony najdete na <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> nebo <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Vložte přímou URL adresu obrázku (svg, png, webp). Ikony najdete na <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> nebo <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Neplatná adresa URL", "invalid_url": "Neplatná adresa URL",
"require_user_email": "Vyžadovat e-mailovou adresu", "require_user_email": "Vyžadovat e-mailovou adresu",
"require_user_email_description": "Vyžaduje, aby uživatelé měli e-mailovou adresu. Pokud je tato možnost deaktivována, uživatelé bez e-mailové adresy nebudou moci používat funkce, které e-mailovou adresu vyžadují." "require_user_email_description": "Vyžaduje, aby uživatelé měli e-mailovou adresu. Pokud je tato možnost deaktivována, uživatelé bez e-mailové adresy nebudou moci používat funkce, které e-mailovou adresu vyžadují.",
"view": "Zobrazit",
"toggle_columns": "Přepínat sloupce",
"locale": "Místní nastavení",
"ldap_id": "LDAP ID",
"reauthentication": "Opětovné ověření",
"clear_filters": "Vymazat filtry"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Indsæt en direkte billed-URL (svg, png, webp). Find ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Indsæt en direkte billed-URL (svg, png, webp). Find ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Ugyldig URL", "invalid_url": "Ugyldig URL",
"require_user_email": "Kræver e-mailadresse", "require_user_email": "Kræver e-mailadresse",
"require_user_email_description": "Kræver, at brugerne har en e-mailadresse. Hvis denne funktion er deaktiveret, kan brugere uden en e-mailadresse ikke bruge funktioner, der kræver en e-mailadresse." "require_user_email_description": "Kræver, at brugerne har en e-mailadresse. Hvis denne funktion er deaktiveret, kan brugere uden en e-mailadresse ikke bruge funktioner, der kræver en e-mailadresse.",
"view": "Vis",
"toggle_columns": "Skift kolonner",
"locale": "Lokale",
"ldap_id": "LDAP-id",
"reauthentication": "Genbekræftelse",
"clear_filters": "Ryd filtre"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Füge eine direkte Bild-URL ein (svg, png, webp). Finde Symbole bei <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> oder <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Füge eine direkte Bild-URL ein (svg, png, webp). Finde Symbole bei <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> oder <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Ungültige URL", "invalid_url": "Ungültige URL",
"require_user_email": "E-Mail-Adresse erforderlich", "require_user_email": "E-Mail-Adresse erforderlich",
"require_user_email_description": "Benutzer müssen eine E-Mail-Adresse haben. Wenn das deaktiviert ist, können Leute ohne E-Mail-Adresse die Funktionen, die eine E-Mail-Adresse brauchen, nicht nutzen." "require_user_email_description": "Benutzer müssen eine E-Mail-Adresse haben. Wenn das deaktiviert ist, können Leute ohne E-Mail-Adresse die Funktionen, die eine E-Mail-Adresse brauchen, nicht nutzen.",
"view": "Ansicht",
"toggle_columns": "Spalten umschalten",
"locale": "Ort",
"ldap_id": "LDAP-ID",
"reauthentication": "Erneute Authentifizierung",
"clear_filters": "Filter löschen"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> or <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> or <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Invalid URL", "invalid_url": "Invalid URL",
"require_user_email": "Require Email Address", "require_user_email": "Require Email Address",
"require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address." "require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address.",
"view": "View",
"toggle_columns": "Toggle columns",
"locale": "Locale",
"ldap_id" : "LDAP ID",
"reauthentication": "Re-authentication",
"clear_filters" : "Clear Filters"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Pega una URL de imagen directa (svg, png, webp). Encuentra iconos en <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Pega una URL de imagen directa (svg, png, webp). Encuentra iconos en <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "URL no válida", "invalid_url": "URL no válida",
"require_user_email": "Requerir dirección de correo electrónico", "require_user_email": "Requerir dirección de correo electrónico",
"require_user_email_description": "Requiere que los usuarios tengan una dirección de correo electrónico. Si se desactiva, los usuarios que no tengan una dirección de correo electrónico no podrán utilizar las funciones que la requieran." "require_user_email_description": "Requiere que los usuarios tengan una dirección de correo electrónico. Si se desactiva, los usuarios que no tengan una dirección de correo electrónico no podrán utilizar las funciones que la requieran.",
"view": "Ver",
"toggle_columns": "Alternar columnas",
"locale": "Configuración regional",
"ldap_id": "Identificador LDAP",
"reauthentication": "Reautenticación",
"clear_filters": "Borrar filtros"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Colle une URL d'image directe (svg, png, webp). Trouve des icônes sur <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Colle une URL d'image directe (svg, png, webp). Trouve des icônes sur <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "URL pas valide", "invalid_url": "URL pas valide",
"require_user_email": "Besoin d'une adresse e-mail", "require_user_email": "Besoin d'une adresse e-mail",
"require_user_email_description": "Les utilisateurs doivent avoir une adresse e-mail. Si cette option est désactivée, ceux qui n'ont pas d'adresse e-mail ne pourront pas utiliser les fonctionnalités qui en ont besoin." "require_user_email_description": "Les utilisateurs doivent avoir une adresse e-mail. Si cette option est désactivée, ceux qui n'ont pas d'adresse e-mail ne pourront pas utiliser les fonctionnalités qui en ont besoin.",
"view": "Voir",
"toggle_columns": "Basculer les colonnes",
"locale": "Paramètres régionaux",
"ldap_id": "ID LDAP",
"reauthentication": "Réauthentification",
"clear_filters": "Effacer les filtres"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Incolla l'URL diretto dell'immagine (svg, png, webp). Trova le icone su <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Incolla l'URL diretto dell'immagine (svg, png, webp). Trova le icone su <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "URL non valido", "invalid_url": "URL non valido",
"require_user_email": "Richiesta indirizzo e-mail", "require_user_email": "Richiesta indirizzo e-mail",
"require_user_email_description": "Chiede agli utenti di avere un indirizzo email. Se disattivato, chi non ha un indirizzo email non potrà usare le funzioni che lo richiedono." "require_user_email_description": "Chiede agli utenti di avere un indirizzo email. Se disattivato, chi non ha un indirizzo email non potrà usare le funzioni che lo richiedono.",
"view": "Visualizza",
"toggle_columns": "Attiva/disattiva colonne",
"locale": "Locale",
"ldap_id": "ID LDAP",
"reauthentication": "Riautenticazione",
"clear_filters": "Cancella filtri"
} }

465
frontend/messages/ja.json Normal file
View File

@@ -0,0 +1,465 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "マイアカウント",
"logout": "ログアウト",
"confirm": "確認",
"docs": "ドキュメント",
"key": "キー",
"value": "値",
"remove_custom_claim": "カスタムクレームを削除",
"add_custom_claim": "カスタムクレームを追加",
"add_another": "さらに追加",
"select_a_date": "日付を選択",
"select_file": "ファイルを選択",
"profile_picture": "プロフィール画像",
"profile_picture_is_managed_by_ldap_server": "プロフィール画像はLDAPサーバーによって管理されており、ここでは変更できません。",
"click_profile_picture_to_upload_custom": "プロフィール画像をクリックして、ファイルからカスタム画像をアップロードします。",
"image_should_be_in_format": "画像はPNGまたはJPEG形式である必要があります。",
"items_per_page": "ページあたりの表示件数",
"no_items_found": "項目が見つかりません",
"select_items": "項目を選択…",
"search": "検索…",
"expand_card": "カードを展開",
"copied": "コピーしました",
"click_to_copy": "クリックしてコピー",
"something_went_wrong": "エラーが発生しました",
"go_back_to_home": "ホームへ戻る",
"alternative_sign_in_methods": "別のサインイン方法",
"login_background": "ログインの背景",
"logo": "ロゴ",
"login_code": "ログインコード",
"create_a_login_code_to_sign_in_without_a_passkey_once": "ユーザーがパスキーなしで一度だけサインインできるログインコードを作成する。",
"one_hour": "1 時間",
"twelve_hours": "12 時間",
"one_day": "1 日",
"one_week": "1 週間",
"one_month": "1 ヶ月",
"expiration": "有効期限",
"generate_code": "コードの生成",
"name": "名前",
"browser_unsupported": "ブラウザ未対応",
"this_browser_does_not_support_passkeys": "このブラウザはパスキーをサポートしていません。別のサインイン方法を使用してください。",
"an_unknown_error_occurred": "不明なエラーが発生しました",
"authentication_process_was_aborted": "認証プロセスが中止されました",
"error_occurred_with_authenticator": "認証ツールでエラーが発生しました",
"authenticator_does_not_support_discoverable_credentials": "認証ツールは discoverable credential をサポートしていません",
"authenticator_does_not_support_resident_keys": "認証ツールは resident key をサポートしていません",
"passkey_was_previously_registered": "このパスキーは既に登録されています",
"authenticator_does_not_support_any_of_the_requested_algorithms": "認証ツールは要求されたアルゴリズムのいずれをもサポートしていません",
"authenticator_timed_out": "認証ツールがタイムアウトしました",
"critical_error_occurred_contact_administrator": "重大なエラーが発生しました。管理者にお問い合わせください。",
"sign_in_to": "{name} にサインイン",
"client_not_found": "クライアントが見つかりません",
"client_wants_to_access_the_following_information": "<b>{client}</b> は以下の情報にアクセスしようとしています:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "{appName} アカウントで <b>{client}</b> にサインインしますか?",
"email": "メール",
"view_your_email_address": "メールアドレスを表示",
"profile": "プロフィール",
"view_your_profile_information": "プロフィール情報を表示",
"groups": "グループ",
"view_the_groups_you_are_a_member_of": "あなたがメンバーであるグループを表示",
"cancel": "キャンセル",
"sign_in": "サインイン",
"try_again": "リトライ",
"client_logo": "クライアントロゴ",
"sign_out": "サインアウト",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "<b>{username}</b> アカウントで {appName} からサインアウトしますか?",
"sign_in_to_appname": "{appName} にサインイン",
"please_try_to_sign_in_again": "もう一度サインインしてください。",
"authenticate_with_passkey_to_access_account": "アカウントにアクセスするには、パスキーで認証してください。",
"authenticate": "認証",
"please_try_again": "再度お試しください。",
"continue": "続行",
"alternative_sign_in": "別のサインイン",
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "パスキーにアクセスできない場合は、以下のいずれかの方法でサインインできます。",
"use_your_passkey_instead": "代わりにパスキーを使用しますか?",
"email_login": "メールでログイン",
"enter_a_login_code_to_sign_in": "ログインコードを入力してサインインしてください。",
"sign_in_with_login_code": "ログインコードでサインイン",
"request_a_login_code_via_email": "メールでログインコードをリクエストする。",
"go_back": "戻る",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "システムに存在する場合、指定されたメールアドレスにメールが送信されました。",
"enter_code": "コードを入力",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "ログインコードのメールを受信するには、メールアドレスを入力してください。",
"your_email": "あなたのメール",
"submit": "送信",
"enter_the_code_you_received_to_sign_in": "サインインするために受け取ったコードを入力してください。",
"code": "コード",
"invalid_redirect_url": "無効な redirect URL",
"audit_log": "監査ログ",
"users": "ユーザー",
"user_groups": "ユーザーグループ",
"oidc_clients": "OIDCクライアント",
"api_keys": "API キー",
"application_configuration": "アプリケーション設定",
"settings": "設定",
"update_pocket_id": "Pocket ID を更新",
"powered_by": "Powered by",
"see_your_account_activities_from_the_last_3_months": "過去3ヶ月間のアカウントアクティビティを確認する。",
"time": "時間",
"event": "イベント",
"approximate_location": "おおよその場所",
"ip_address": "IPアドレス",
"device": "デバイス",
"client": "クライアント",
"unknown": "不明",
"account_details_updated_successfully": "アカウントの詳細が正常に更新されました",
"profile_picture_updated_successfully": "プロフィール画像が正常に更新されました。反映まで数分かかる場合があります。",
"account_settings": "アカウント設定",
"passkey_missing": "パスキーが見つかりません",
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "アカウントへのアクセスを失わないように、パスキーを追加してください。",
"single_passkey_configured": "Single Passkey Configured",
"it_is_recommended_to_add_more_than_one_passkey": "アカウントへのアクセスを失わないように、複数のパスキーを追加することをお勧めします。",
"account_details": "アカウントの詳細",
"passkeys": "パスキー",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "認証に使用するパスキーを管理します。",
"add_passkey": "パスキーを追加",
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "パスキーなしで別のデバイスからサインインするためのワンタイムログインコードを作成します。",
"create": "作成",
"first_name": "名",
"last_name": "姓",
"username": "ユーザー名",
"save": "保存",
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
"username_must_start_with": "Username must start with an alphanumeric character",
"username_must_end_with": "Username must end with an alphanumeric character",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "以下のコードを使用してサインインしてください。コードは15分後に失効します。",
"or_visit": "or visit",
"added_on": "追加日",
"rename": "名前を変更",
"delete": "削除",
"are_you_sure_you_want_to_delete_this_passkey": "このパスキーを削除してもよろしいですか?",
"passkey_deleted_successfully": "パスキーが正常に削除されました",
"delete_passkey_name": "{passkeyName} を削除",
"passkey_name_updated_successfully": "パスキー名が正常に更新されました",
"name_passkey": "パスキー名",
"name_your_passkey_to_easily_identify_it_later": "パスキーを後で簡単に識別するように名前を付けます。",
"create_api_key": "API キーを作成",
"add_a_new_api_key_for_programmatic_access": "<link href='https://pocket-id.org/docs/api'>Pocket ID API</link> へのプログラムによるアクセス用に新しいAPI キーを追加します。",
"add_api_key": "API キーを追加",
"manage_api_keys": "API キーの管理",
"api_key_created": "APIキー が作成されました",
"for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.",
"description": "説明",
"api_key": "API キー",
"close": "閉じる",
"name_to_identify_this_api_key": "この API キーを識別するための名前。",
"expires_at": "有効期限",
"when_this_api_key_will_expire": "このAPI キーが期限切れになる日付。",
"optional_description_to_help_identify_this_keys_purpose": "このキーの目的を特定するのに役立つ任意の説明。",
"expiration_date_must_be_in_the_future": "有効期限は未来の日付にする必要があります",
"revoke_api_key": "API キーの削除",
"never": "なし",
"revoke": "削除",
"api_key_revoked_successfully": "API キーが正常に削除されました",
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "APIキー \"{apiKeyName}\" を本当に削除しますかこのAPI キーを使用しているすべての連携が停止します。",
"last_used": "最終使用日",
"actions": "Actions",
"images_updated_successfully": "画像が正常に更新されました",
"general": "一般",
"configure_smtp_to_send_emails": "新しいデバイスや場所からのログインが検出された際にユーザーに警告するメール通知を有効にします。",
"ldap": "LDAP",
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
"images": "画像",
"update": "更新",
"email_configuration_updated_successfully": "メール設定が正常に更新されました",
"save_changes_question": "変更を保存しますか?",
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?",
"save_and_send": "保存して送信",
"test_email_sent_successfully": "テストメールがあなたのメールアドレスに正常に送信されました。",
"failed_to_send_test_email": "テストメールの送信に失敗しました。詳細についてはサーバーのログを確認してください。",
"smtp_configuration": "SMTP 設定",
"smtp_host": "SMTP ホスト",
"smtp_port": "SMTP ポート",
"smtp_user": "SMTP ユーザー",
"smtp_password": "SMTP パスワード",
"smtp_from": "SMTP 送信元",
"smtp_tls_option": "SMTP TLS オプション",
"email_tls_option": "メール TLS オプション",
"skip_certificate_verification": "Skip Certificate Verification",
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
"enabled_emails": "有効なメール",
"email_login_notification": "メールログイン通知",
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "ユーザーが新しいデバイスからログインした際にメールを送信する。",
"emai_login_code_requested_by_user": "ユーザーがリクエストしたメールログインコード",
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "ユーザーがメールに送信されたログインコードをリクエストすることでパスキーをバイパスできるようにします。これによりセキュリティが大幅に低下し、ユーザーのメールにアクセスできる者なら誰でもログイン可能になります。",
"email_login_code_from_admin": "管理者からのメールログインコード",
"allows_an_admin_to_send_a_login_code_to_the_user": "管理者がメールでログインコードをユーザーに送信することを許可します。",
"send_test_email": "テストメールを送信",
"application_configuration_updated_successfully": "アプリケーション設定が正常に更新されました",
"application_name": "アプリケーション名",
"session_duration": "セッション期間",
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "ユーザーが再度ログインする必要があるまでのセッションの継続時間。(分単位)",
"enable_self_account_editing": "自身のアカウント編集を有効にする",
"whether_the_users_should_be_able_to_edit_their_own_account_details": "ユーザーが自身のアカウントの詳細を編集できるかどうか。",
"emails_verified": "Emails Verified",
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Whether the user's email should be marked as verified for the OIDC clients.",
"ldap_configuration_updated_successfully": "LDAP 設定が正常に更新されました",
"ldap_disabled_successfully": "LDAPは正常に無効化されました",
"ldap_sync_finished": "LDAP同期が完了しました",
"client_configuration": "クライアントの設定",
"ldap_url": "LDAP URL",
"ldap_bind_dn": "LDAP Bind DN",
"ldap_bind_password": "LDAP Bind Password",
"ldap_base_dn": "LDAP Base DN",
"user_search_filter": "ユーザー検索フィルター",
"the_search_filter_to_use_to_search_or_sync_users": "ユーザーの検索、同期に使用する検索フィルター。",
"groups_search_filter": "グループ検索フィルター",
"the_search_filter_to_use_to_search_or_sync_groups": "グループの検索、同期に使用する検索フィルター。",
"attribute_mapping": "属性マッピング",
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
"the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.",
"username_attribute": "ユーザー名属性",
"user_mail_attribute": "ユーザーメール属性",
"user_first_name_attribute": "User First Name Attribute",
"user_last_name_attribute": "User Last Name Attribute",
"user_profile_picture_attribute": "ユーザープロフィール画像属性",
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "この属性の値は、URL、バイナリ、またはBase64エンコードされた画像のいずれかです。",
"group_members_attribute": "グループメンバー属性",
"the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.",
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
"group_rdn_attribute": "Group RDN Attribute (in DN)",
"admin_group_name": "管理者グループ名",
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.",
"disable": "無効",
"sync_now": "今すぐ同期",
"enable": "有効",
"user_created_successfully": "ユーザーは正常に作成されました",
"create_user": "ユーザーを作成",
"add_a_new_user_to_appname": "{appName} に新しいユーザーを追加",
"add_user": "ユーザーを追加",
"manage_users": "ユーザー管理",
"admin_privileges": "管理者権限",
"admins_have_full_access_to_the_admin_panel": "管理者は管理パネルへのフルアクセス権限を持っています。",
"delete_firstname_lastname": "削除 {firstName} {lastName}",
"are_you_sure_you_want_to_delete_this_user": "このユーザーを削除してもよろしいですか?",
"user_deleted_successfully": "ユーザーの削除が正常に完了しました",
"role": "ロール",
"source": "Source",
"admin": "管理者",
"user": "ユーザー",
"local": "ローカル",
"toggle_menu": "Toggle menu",
"edit": "編集",
"user_groups_updated_successfully": "ユーザーグループが正常に更新されました",
"user_updated_successfully": "ユーザーは正常に更新されました",
"custom_claims_updated_successfully": "カスタムクレームが正常に更新されました",
"back": "Back",
"user_details_firstname_lastname": "ユーザーの詳細 {firstName} {lastName}",
"manage_which_groups_this_user_belongs_to": "このユーザーが所属するグループを管理します。",
"custom_claims": "カスタムクレーム",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。",
"user_group_created_successfully": "ユーザーグループが正常に作成されました",
"create_user_group": "ユーザーグループを作成",
"create_a_new_group_that_can_be_assigned_to_users": "ユーザーに割り当て可能な新しいグループを作成します。",
"add_group": "グループを追加",
"manage_user_groups": "ユーザーグループの管理",
"friendly_name": "Friendly Name",
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
"delete_name": "{name} を削除",
"are_you_sure_you_want_to_delete_this_user_group": "このユーザーグループを削除しますか?",
"user_group_deleted_successfully": "ユーザーグループが正常に削除されました",
"user_count": "ユーザー数",
"user_group_updated_successfully": "ユーザーグループが正常に更新されました",
"users_updated_successfully": "ユーザーは正常に更新されました",
"user_group_details_name": "ユーザーグループの詳細 {name}",
"assign_users_to_this_group": "このグループにユーザーを割り当てます。",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。競合が発生した場合、ユーザーに定義されたカスタムクレームが優先されます。",
"oidc_client_created_successfully": "OIDC client created successfully",
"create_oidc_client": "OIDC クライアントの作成",
"add_a_new_oidc_client_to_appname": "{appName} に新しい OIDC クライアントを追加します。",
"add_oidc_client": "OIDC クライアントを追加",
"manage_oidc_clients": "OIDC クライアントの管理",
"one_time_link": "ワンタイムリンク",
"use_this_link_to_sign_in_once": "このリンクを使用してサインインしてください。これは、まだパスキーを追加していないユーザーや、パスキーを紛失したユーザーに必要な手順です。",
"add": "追加",
"callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client",
"public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
"requires_reauthentication": "再認証が必要",
"requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in",
"name_logo": "{name} ロゴ",
"change_logo": "ロゴの変更",
"upload_logo": "ロゴをアップロード",
"remove_logo": "ロゴを削除",
"are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?",
"oidc_client_deleted_successfully": "OIDC client deleted successfully",
"authorization_url": "Authorization URL",
"oidc_discovery_url": "OIDC Discovery URL",
"token_url": "Token URL",
"userinfo_url": "Userinfo URL",
"logout_url": "Logout URL",
"certificate_url": "Certificate URL",
"enabled": "有効",
"disabled": "無効",
"oidc_client_updated_successfully": "OIDC クライアントが正常に更新されました",
"create_new_client_secret": "新しいクライアントシークレットを作成する",
"are_you_sure_you_want_to_create_a_new_client_secret": "新しいクライアントシークレットを作成してもよろしいですか?古いシークレットは無効化されます。",
"generate": "Generate",
"new_client_secret_created_successfully": "新しいクライアントシークレットが正常に作成されました",
"allowed_user_groups_updated_successfully": "許可されたユーザーグループが正常に更新されました",
"oidc_client_name": "OIDC クライアント {name}",
"client_id": "Client ID",
"client_secret": "クライアントシークレット",
"show_more_details": "詳細を表示",
"allowed_user_groups": "許可されたユーザーグループ",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
"favicon": "Favicon",
"light_mode_logo": "ライトモードのロゴ",
"dark_mode_logo": "ダークモードのロゴ",
"background_image": "背景画像",
"language": "言語",
"reset_profile_picture_question": "プロフィール画像をリセットしますか?",
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "アップロードされた画像を削除し、プロフィール画像をデフォルトにリセットします。続行しますか?",
"reset": "リセット",
"reset_to_default": "デフォルトに戻す",
"profile_picture_has_been_reset": "プロフィール画像がリセットされました。更新には数分かかる場合があります。",
"select_the_language_you_want_to_use": "使用する言語を選択してください。一部のテキストは自動翻訳により、不正確な場合がありますのでご注意ください。",
"contribute_to_translation": "問題が見つかった場合は、 <link href='https://crowdin.com/project/pocket-id'>Crowdin</link> で翻訳に貢献してください。",
"personal": "Personal",
"global": "Global",
"all_users": "すべてのユーザー",
"all_events": "すべてのイベント",
"all_clients": "すべてのクライアント",
"all_locations": "すべての場所",
"global_audit_log": "グローバル監査ログ",
"see_all_account_activities_from_the_last_3_months": "過去3ヶ月間のすべてのユーザーアクティビティを表示します。",
"token_sign_in": "トークンサインイン",
"client_authorization": "Client Authorization",
"new_client_authorization": "New Client Authorization",
"disable_animations": "アニメーションの無効化",
"turn_off_ui_animations": "Turn off animations throughout the UI.",
"user_disabled": "アカウントの無効化",
"disabled_users_cannot_log_in_or_use_services": "無効化されたユーザーはログインやサービスを利用できません。",
"user_disabled_successfully": "ユーザーが正常に無効化されました。",
"user_enabled_successfully": "ユーザーが正常に有効化されました。",
"status": "Status",
"disable_firstname_lastname": "無効化 {firstName} {lastName}",
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
"ldap_soft_delete_users": "LDAPから無効なユーザーを保持します。",
"ldap_soft_delete_users_description": "有効にすると、LDAPから削除されたユーザーはシステムから削除されるのではなく、無効化されます。",
"login_code_email_success": "ログインコードがユーザーに送信されました。",
"send_email": "メールを送信",
"show_code": "コードを表示",
"callback_url_description": "クライアントが提供するURL。空白のままにすると自動的に追加されます。ワイルドカード(*)はサポートされていますが、セキュリティを向上させるためには避けてください。",
"logout_callback_url_description": "クライアントがログアウト用に提供するURL。ワイルドカード(*)はサポートされていますが、セキュリティを向上させるためには避けてください。",
"api_key_expiration": "API キーの有効期限",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API キーの有効期限が近づいたら、ユーザーにメールを送信します。",
"authorize_device": "デバイスの認証",
"the_device_has_been_authorized": "デバイスは認証されました。",
"enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。",
"authorize": "Authorize",
"federated_client_credentials": "Federated Client Credentials",
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
"add_federated_client_credential": "Add Federated Client Credential",
"add_another_federated_client_credential": "Add another federated client credential",
"oidc_allowed_group_count": "許可されたグループ数",
"unrestricted": "制限なし",
"show_advanced_options": "詳細設定を表示",
"hide_advanced_options": "詳細設定を隠す",
"oidc_data_preview": "OIDC Data Preview",
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
"id_token": "ID Token",
"access_token": "Access Token",
"userinfo": "Userinfo",
"id_token_payload": "ID Token Payload",
"access_token_payload": "Access Token Payload",
"userinfo_endpoint_response": "Userinfo Endpoint Response",
"copy": "コピー",
"no_preview_data_available": "No preview data available",
"copy_all": "すべてコピー",
"preview": "プレビュー",
"preview_for_user": "{name} のプレビュー",
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
"show": "表示",
"select_an_option": "Select an option",
"select_user": "ユーザーを選択",
"error": "エラー",
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "アクセントカラーを選択して、Pocket IDの外観をカスタマイズしてください。",
"accent_color": "アクセントカラー",
"custom_accent_color": "カスタムアクセントカラー",
"custom_accent_color_description": "有効な CSS カラーフォーマット (例: hex, rgb, hsl) を使用してカスタムカラーを入力します。",
"color_value": "Color Value",
"apply": "適用",
"signup_token": "サインアップトークン",
"create_a_signup_token_to_allow_new_user_registration": "新規ユーザー登録を許可するためのサインアップトークンを作成する。",
"usage_limit": "使用回数制限",
"number_of_times_token_can_be_used": "サインアップトークンが使用できる回数。",
"expires": "Expires",
"signup": "サインアップ",
"user_creation": "ユーザー作成",
"configure_user_creation": "新しいユーザーのサインアップ方法やデフォルトの権限など、ユーザー作成設定を管理します。",
"user_creation_groups_description": "新しいユーザーにこれらのグループを自動的に割り当てます。",
"user_creation_claims_description": "新しいユーザーにこれらのカスタムクレームを自動的に割り当てます。",
"user_creation_updated_successfully": "ユーザー作成設定が正常に更新されました。",
"signup_disabled_description": "ユーザーのサインアップは無効化されています。管理者のみが新しいユーザーアカウントを作成できます。",
"signup_requires_valid_token": "アカウントを作成するには有効なサインアップトークンが必要です",
"validating_signup_token": "サインアップトークンを検証中",
"go_to_login": "ログインへ移動",
"signup_to_appname": "{appName} にサインアップ",
"create_your_account_to_get_started": "アカウントを作成して始めましょう。",
"initial_account_creation_description": "ご利用を開始するにはアカウントを作成してください。パスキーの設定は後で可能です。",
"setup_your_passkey": "パスキーを設定",
"create_a_passkey_to_securely_access_your_account": "安全にアカウントにアクセスするためのパスキーを作成します。これがサインインの主な方法になります。",
"skip_for_now": "あとで",
"account_created": "アカウントが作成されました",
"enable_user_signups": "ユーザーサインアップを有効にする",
"enable_user_signups_description": "Pocket IDでユーザーが新規アカウントを登録する方法を決定します。",
"user_signups_are_disabled": "ユーザーのサインアップは現在無効化されています",
"create_signup_token": "サインアップトークンを作成",
"view_active_signup_tokens": "有効なサインアップトークンを表示",
"manage_signup_tokens": "サインアップトークンの管理",
"view_and_manage_active_signup_tokens": "有効なサインアップトークンの表示と管理。",
"signup_token_deleted_successfully": "サインアップトークンが正常に削除されました。",
"expired": "Expired",
"used_up": "Used Up",
"active": "Active",
"usage": "Usage",
"created": "Created",
"token": "トークン",
"loading": "読み込み中",
"delete_signup_token": "サインアップトークンを削除",
"are_you_sure_you_want_to_delete_this_signup_token": "このサインアップトークンを削除してもよろしいですか?この操作は元に戻せません。",
"signup_with_token": "トークンでサインアップ",
"signup_with_token_description": "ユーザーは管理者が作成した有効なサインアップトークンを使用してのみサインアップできます。",
"signup_open": "サインアップを開く",
"signup_open_description": "誰でも制限なしに新しいアカウントを作成できます。",
"of": "of",
"skip_passkey_setup": "パスキーの設定をスキップ",
"skip_passkey_setup_description": "パスキーの設定を強く推奨します。設定していない場合、セッションが切れた時点でアカウントにアクセスできなくなります。",
"my_apps": "マイアプリ",
"no_apps_available": "利用可能なアプリはありません",
"contact_your_administrator_for_app_access": "アプリケーションにアクセスするには、管理者に問い合わせてください。",
"launch": "起動",
"client_launch_url": "Client Launch URL",
"client_launch_url_description": "ユーザーが「マイアプリ」ページからアプリを起動した際に開かれるURL。",
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
"revoke_access": "アクセスを取り消す",
"revoke_access_description": "<b>{clientName}</b>へのアクセスを取り消します。 <b>{clientName}</b> はあなたのアカウント情報にアクセスできなくなります。",
"revoke_access_successful": "{clientName} へのアクセスは正常に取り消されました。",
"last_signed_in_ago": "Last signed in {time} ago",
"invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens",
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
"generated": "Generated",
"administration": "Administration",
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
"display_name_attribute": "Display Name Attribute",
"display_name": "表示名",
"configure_application_images": "アプリケーションの画像を設定",
"ui_config_disabled_info_title": "UI Configuration Disabled",
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.",
"logo_from_url_description": "画像の直接のURL (svg, png, webp) を貼り付けます。アイコンは <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> か <link href=\"https://dashboardicons.com\">Dashboard Icons</link> で探せます。",
"invalid_url": "無効な URL",
"require_user_email": "Require Email Address",
"require_user_email_description": "ユーザーにメールアドレスの登録を必須とします。無効にした場合、メールアドレスを持たないユーザーはメールアドレスが必要な機能を利用できなくなります。",
"view": "表示",
"toggle_columns": "列の表示/非表示を切り替える",
"locale": "ロケール",
"ldap_id": "LDAP ID",
"reauthentication": "再認証",
"clear_filters": "フィルターをクリア"
}

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "이미지 다이렉트 URL (svg, png, webp)을 붙여넣으세요. <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> 또는 <link href=\"https://dashboardicons.com\">Dashboard Icons</link>에서 아이콘을 찾으세요.", "logo_from_url_description": "이미지 다이렉트 URL (svg, png, webp)을 붙여넣으세요. <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> 또는 <link href=\"https://dashboardicons.com\">Dashboard Icons</link>에서 아이콘을 찾으세요.",
"invalid_url": "잘못된 URL", "invalid_url": "잘못된 URL",
"require_user_email": "이메일 주소 필수", "require_user_email": "이메일 주소 필수",
"require_user_email_description": "사용자에게 이메일 주소가 있어야 합니다. 이 설정이 비활성화되면 이메일 주소가 없는 사용자는 이메일 주소가 필요한 기능을 사용할 수 없습니다." "require_user_email_description": "사용자에게 이메일 주소가 있어야 합니다. 이 설정이 비활성화되면 이메일 주소가 없는 사용자는 이메일 주소가 필요한 기능을 사용할 수 없습니다.",
"view": "보기",
"toggle_columns": "열 표시/숨기기",
"locale": "로케일",
"ldap_id": "LDAP ID",
"reauthentication": "재인증",
"clear_filters": "필터 지우기"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Plak een directe afbeeldings-URL (svg, png, webp). Zoek pictogrammen op <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> of <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Plak een directe afbeeldings-URL (svg, png, webp). Zoek pictogrammen op <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> of <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Ongeldige URL", "invalid_url": "Ongeldige URL",
"require_user_email": "E-mailadres vereist", "require_user_email": "E-mailadres vereist",
"require_user_email_description": "Je moet een e-mailadres hebben. Als je dit uitschakelt, kunnen mensen zonder e-mailadres geen functies gebruiken waarvoor een e-mailadres nodig is." "require_user_email_description": "Je moet een e-mailadres hebben. Als je dit uitschakelt, kunnen mensen zonder e-mailadres geen functies gebruiken waarvoor een e-mailadres nodig is.",
"view": "Bekijken",
"toggle_columns": "Kolommen wisselen",
"locale": "Locatie",
"ldap_id": "LDAP-ID",
"reauthentication": "Opnieuw inloggen",
"clear_filters": "Filters wissen"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Wklej bezpośredni adres URL obrazu (svg, png, webp). Znajdź ikony na stronie <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> lub <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Wklej bezpośredni adres URL obrazu (svg, png, webp). Znajdź ikony na stronie <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> lub <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Nieprawidłowy adres URL", "invalid_url": "Nieprawidłowy adres URL",
"require_user_email": "Wymagany adres e-mail", "require_user_email": "Wymagany adres e-mail",
"require_user_email_description": "Wymaga od was posiadania adresu e-mail. Jeśli opcja zostanie wyłączona, wy bez adresu e-mail nie będziecie mogli korzystać z funkcji wymagających adresu e-mail." "require_user_email_description": "Wymaga od was posiadania adresu e-mail. Jeśli opcja zostanie wyłączona, wy bez adresu e-mail nie będziecie mogli korzystać z funkcji wymagających adresu e-mail.",
"view": "Widok",
"toggle_columns": "Przełącz kolumny",
"locale": "Ustawienia regionalne",
"ldap_id": "Identyfikator LDAP",
"reauthentication": "Ponowne uwierzytelnianie",
"clear_filters": "Wyczyść filtry"
} }

View File

@@ -28,7 +28,7 @@
"login_background": "Histórico de login", "login_background": "Histórico de login",
"logo": "Logotipo", "logo": "Logotipo",
"login_code": "Código de Login:", "login_code": "Código de Login:",
"create_a_login_code_to_sign_in_without_a_passkey_once": "Crie um código de login que o usuário possa usar para entrar sem precisar digitar uma senha.", "create_a_login_code_to_sign_in_without_a_passkey_once": "Crie um código de login de uso único para que o usuário possa entrar sem precisar de uma chave de acesso.",
"one_hour": "1 hora", "one_hour": "1 hora",
"twelve_hours": "12 horas", "twelve_hours": "12 horas",
"one_day": "1 dia", "one_day": "1 dia",
@@ -44,46 +44,46 @@
"error_occurred_with_authenticator": "Ocorreu um erro com o autenticador", "error_occurred_with_authenticator": "Ocorreu um erro com o autenticador",
"authenticator_does_not_support_discoverable_credentials": "O autenticador não suporta credenciais detectáveis", "authenticator_does_not_support_discoverable_credentials": "O autenticador não suporta credenciais detectáveis",
"authenticator_does_not_support_resident_keys": "O autenticador não aceita chaves residentes.", "authenticator_does_not_support_resident_keys": "O autenticador não aceita chaves residentes.",
"passkey_was_previously_registered": "Essa chave mestra já foi registrada antes.", "passkey_was_previously_registered": "Esta chave de acesso já está registrada",
"authenticator_does_not_support_any_of_the_requested_algorithms": "O autenticador não suporta nenhum dos algoritmos solicitados.", "authenticator_does_not_support_any_of_the_requested_algorithms": "O autenticador não suporta nenhum dos algoritmos solicitados.",
"authenticator_timed_out": "Tempo limite do autenticador atingido", "authenticator_timed_out": "Tempo limite do autenticador atingido",
"critical_error_occurred_contact_administrator": "Ocorreu um erro grave. Por favor, entre em contato com o administrador.", "critical_error_occurred_contact_administrator": "Ocorreu um erro grave. Por favor, entre em contato com o administrador.",
"sign_in_to": "Entrar em {name}", "sign_in_to": "Entrar em {name}",
"client_not_found": "Cliente não encontrado", "client_not_found": "Cliente não encontrado",
"client_wants_to_access_the_following_information": "<b>{client}</b> quer acessar as seguintes informações:", "client_wants_to_access_the_following_information": "<b>{client}</b> quer acessar as seguintes informações:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Você quer entrar em <b>{client}</b> com a sua conta {appName}?", "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Deseja entrar em <b>{client}</b> com a sua conta {appName}?",
"email": "E-mail", "email": "E-mail",
"view_your_email_address": "Ver seu endereço de e-mail", "view_your_email_address": "Ver seu endereço de e-mail",
"profile": "Perfil", "profile": "Perfil",
"view_your_profile_information": "Dá uma olhada nas informações do seu perfil", "view_your_profile_information": "Veja as informações do seu perfil",
"groups": "Grupos", "groups": "Grupos",
"view_the_groups_you_are_a_member_of": "Dá uma olhada nos grupos que você faz parte", "view_the_groups_you_are_a_member_of": "Veja os grupos que você faz parte",
"cancel": "Cancelar", "cancel": "Cancelar",
"sign_in": "Entrar", "sign_in": "Entrar",
"try_again": "Tentar novamente", "try_again": "Tentar novamente",
"client_logo": "Logo do Cliente", "client_logo": "Logo do Cliente",
"sign_out": "Sair", "sign_out": "Sair",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?", "do_you_want_to_sign_out_of_pocketid_with_the_account": "Deseja sair de {appName} com a conta <b>{username}</b>?",
"sign_in_to_appname": "Entrar em {appName}", "sign_in_to_appname": "Entrar em {appName}",
"please_try_to_sign_in_again": "Tenta entrar de novo.", "please_try_to_sign_in_again": "Tente entrar novamente.",
"authenticate_with_passkey_to_access_account": "Autentique-se com sua chave de acesso para entrar na sua conta.", "authenticate_with_passkey_to_access_account": "Identifique-se com sua chave de acesso para entrar em sua conta.",
"authenticate": "Autenticar", "authenticate": "Autenticar",
"please_try_again": "Tenta de novo, por favor.", "please_try_again": "Por favor tente novamente.",
"continue": "Continuar", "continue": "Continuar",
"alternative_sign_in": "Entrar de outra forma", "alternative_sign_in": "Entrar de outra forma",
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Se você não tem acesso à sua chave de acesso, pode entrar usando um dos métodos a seguir.", "if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Se perdeu sua chave de acesso, pode entrar usando um dos métodos a seguir.",
"use_your_passkey_instead": "Quer usar sua chave de acesso?", "use_your_passkey_instead": "Quer usar sua chave de acesso?",
"email_login": "Entrar com e-mail", "email_login": "Entrar com e-mail",
"enter_a_login_code_to_sign_in": "Digite um código de login para entrar.", "enter_a_login_code_to_sign_in": "Digite um código de login para entrar.",
"sign_in_with_login_code": "Faça login com o código de acesso", "sign_in_with_login_code": "Faça login com o código de acesso",
"request_a_login_code_via_email": "Pede um código de login por e-mail.", "request_a_login_code_via_email": "Pede um código de login por e-mail.",
"go_back": "Voltar", "go_back": "Voltar",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Mandamos um e-mail pro endereço que você deu, se ele estiver no nosso sistema.", "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "E-mail enviado para o endereço fornecido, se ele existir no sistema.",
"enter_code": "Digite o código", "enter_code": "Digite o código",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Digite seu e-mail pra receber um e-mail com um código de login.", "enter_your_email_address_to_receive_an_email_with_a_login_code": "Digite seu e-mail para receber um código de login.",
"your_email": "Seu e-mail", "your_email": "Seu e-mail",
"submit": "Enviar", "submit": "Enviar",
"enter_the_code_you_received_to_sign_in": "Digite o código que você recebeu para entrar.", "enter_the_code_you_received_to_sign_in": "Para entrar, digite o código que recebeu.",
"code": "Código", "code": "Código",
"invalid_redirect_url": "URL de redirecionamento inválido", "invalid_redirect_url": "URL de redirecionamento inválido",
"audit_log": "Registro de Auditoria", "audit_log": "Registro de Auditoria",
@@ -95,7 +95,7 @@
"settings": "Configurações", "settings": "Configurações",
"update_pocket_id": "Atualizar Pocket ID", "update_pocket_id": "Atualizar Pocket ID",
"powered_by": "Fornecido por", "powered_by": "Fornecido por",
"see_your_account_activities_from_the_last_3_months": "Veja suas atividades de conta dos últimos 3 meses.", "see_your_account_activities_from_the_last_3_months": "Veja as atividades da conta nos últimos 3 meses.",
"time": "Hora", "time": "Hora",
"event": "Evento", "event": "Evento",
"approximate_location": "Localização Aproximada", "approximate_location": "Localização Aproximada",
@@ -104,34 +104,34 @@
"client": "Cliente", "client": "Cliente",
"unknown": "Desconhecido", "unknown": "Desconhecido",
"account_details_updated_successfully": "Detalhes da conta atualizados com sucesso", "account_details_updated_successfully": "Detalhes da conta atualizados com sucesso",
"profile_picture_updated_successfully": "Foto do perfil atualizada com sucesso. Pode demorar alguns minutos para atualizar.", "profile_picture_updated_successfully": "Foto de perfil alterada com sucesso. A atualização pode demorar alguns minutos.",
"account_settings": "Configurações de Conta", "account_settings": "Configurações de Conta",
"passkey_missing": "Chave de acesso ausente", "passkey_missing": "Chave de acesso ausente",
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Adicione uma senha para evitar perder o acesso à sua conta.", "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Adicione uma chave de acesso para evitar perder o acesso à sua conta.",
"single_passkey_configured": "Chave única configurada", "single_passkey_configured": "Uma chave de acesso configurada",
"it_is_recommended_to_add_more_than_one_passkey": melhor adicionar mais de uma senha de acesso pra evitar perder o acesso à sua conta.", "it_is_recommended_to_add_more_than_one_passkey": recomendado adicionar mais de uma chave de acesso para evitar perder o acesso à sua conta.",
"account_details": "Detalhes da Conta", "account_details": "Detalhes da Conta",
"passkeys": "Chaves-mestras", "passkeys": "Chaves de acesso",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gerencie as chaves de acesso que você pode usar para se autenticar.", "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gerencie as chaves de acesso que pode usar para se identificar.",
"add_passkey": "Adicionar chave de acesso", "add_passkey": "Adicionar chave de acesso",
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Crie um código de login único para entrar em outro dispositivo sem precisar de uma senha.", "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Crie um código de login único para entrar em outro dispositivo sem precisar da chave de acesso.",
"create": "Criar", "create": "Criar",
"first_name": "Primeiro nome", "first_name": "Nome",
"last_name": "Último nome", "last_name": "Sobrenome",
"username": "Nome de usuário", "username": "Nome de usuário",
"save": "Salvar", "save": "Salvar",
"username_can_only_contain": "O nome de usuário só pode conter letras minúsculas, números, underscores, pontos, hífens e símbolos '@'", "username_can_only_contain": "O nome de usuário só pode conter letras minúsculas, números, subtraço, pontos, hífens e símbolos '@'",
"username_must_start_with": "O nome de usuário precisa começar com um caractere alfanumérico.", "username_must_start_with": "O nome de usuário precisa começar com um caractere alfanumérico.",
"username_must_end_with": "O nome de usuário precisa terminar com um caractere alfanumérico.", "username_must_end_with": "O nome de usuário precisa terminar com um caractere alfanumérico.",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Faça o login usando o código a seguir. O código irá expirar em 15 minutos.", "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Faça o login usando o código a seguir. O código irá expirar em 15 minutos.",
"or_visit": "ou visite", "or_visit": "ou visite",
"added_on": "Adicionado em", "added_on": "Adicionado em",
"rename": "Renomear", "rename": "Renomear",
"delete": "Apagar", "delete": "Deletar",
"are_you_sure_you_want_to_delete_this_passkey": "Tem certeza que quer apagar essa chave de acesso?", "are_you_sure_you_want_to_delete_this_passkey": "Tem certeza que deseja deletar esta chave de acesso?",
"passkey_deleted_successfully": "Chave de acesso apagada com sucesso", "passkey_deleted_successfully": "Chave de acesso deletada com sucesso",
"delete_passkey_name": "Apagar {passkeyName}", "delete_passkey_name": "Deletar {passkeyName}",
"passkey_name_updated_successfully": "Nome da chave de acesso atualizado com sucesso", "passkey_name_updated_successfully": "Nome da chave de acesso atualizada com sucesso",
"name_passkey": "Nome da chave de acesso", "name_passkey": "Nome da chave de acesso",
"name_your_passkey_to_easily_identify_it_later": "Dê um nome à sua chave de acesso para identificá-la facilmente mais tarde.", "name_your_passkey_to_easily_identify_it_later": "Dê um nome à sua chave de acesso para identificá-la facilmente mais tarde.",
"create_api_key": "Criar chave API", "create_api_key": "Criar chave API",
@@ -139,20 +139,20 @@
"add_api_key": "Adicionar chave API", "add_api_key": "Adicionar chave API",
"manage_api_keys": "Gerenciar chaves API", "manage_api_keys": "Gerenciar chaves API",
"api_key_created": "Chave API criada", "api_key_created": "Chave API criada",
"for_security_reasons_this_key_will_only_be_shown_once": "Por segurança, essa chave só vai aparecer uma vez. Guarde-a em um lugar seguro.", "for_security_reasons_this_key_will_only_be_shown_once": "Por segurança, esta chave só será exibida uma vez. Salve em um lugar seguro.",
"description": "Descrição", "description": "Descrição",
"api_key": "Chave API", "api_key": "Chave API",
"close": "Fechar", "close": "Fechar",
"name_to_identify_this_api_key": "Nome pra identificar essa chave API.", "name_to_identify_this_api_key": "Nome para identificar esta chave API.",
"expires_at": "Vence em", "expires_at": "Valido até",
"when_this_api_key_will_expire": "Quando essa chave API vai expirar.", "when_this_api_key_will_expire": "Quando esta chave API vai expirar.",
"optional_description_to_help_identify_this_keys_purpose": "Descrição opcional para ajudar a identificar a finalidade desta chave.", "optional_description_to_help_identify_this_keys_purpose": "Descrição opcional para ajudar a identificar a finalidade desta chave.",
"expiration_date_must_be_in_the_future": "A data de validade precisa ser no futuro.", "expiration_date_must_be_in_the_future": "A data de validade precisa ser no futuro.",
"revoke_api_key": "Revogar chave API", "revoke_api_key": "Revogar chave API",
"never": "Nunca", "never": "Nunca",
"revoke": "Revogar", "revoke": "Revogar",
"api_key_revoked_successfully": "Chave API revogada com sucesso", "api_key_revoked_successfully": "Chave API revogada com sucesso",
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Tem certeza que quer cancelar a chave API “{apiKeyName}”? Isso vai desligar todas as integrações que usam essa chave.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Tem certeza que deseja cancelar a chave API “{apiKeyName}”? Isto irá desligar todas as integrações que usam esta chave.",
"last_used": "Último uso", "last_used": "Último uso",
"actions": "Ações", "actions": "Ações",
"images_updated_successfully": "Imagens atualizadas com sucesso", "images_updated_successfully": "Imagens atualizadas com sucesso",
@@ -164,10 +164,10 @@
"update": "Atualização", "update": "Atualização",
"email_configuration_updated_successfully": "Configuração do e-mail atualizada com sucesso", "email_configuration_updated_successfully": "Configuração do e-mail atualizada com sucesso",
"save_changes_question": "Salvar alterações?", "save_changes_question": "Salvar alterações?",
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Você precisa salvar as alterações antes de enviar um e-mail de teste. Quer salvar agora?", "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "É necessário salvar as alterações antes de enviar um e-mail de teste. Deseja salvar agora?",
"save_and_send": "Salvar e enviar", "save_and_send": "Salvar e enviar",
"test_email_sent_successfully": "E-mail de teste enviado com sucesso para o seu endereço de e-mail.", "test_email_sent_successfully": "E-mail de teste enviado com sucesso para o seu endereço de e-mail.",
"failed_to_send_test_email": "Não deu certo enviar o e-mail de teste. Dá uma olhada nos registros do servidor pra saber mais.", "failed_to_send_test_email": "Falha ao enviar o e-mail de teste. Verifique os registros no servidor para mais detalhes.",
"smtp_configuration": "Configuração SMTP", "smtp_configuration": "Configuração SMTP",
"smtp_host": "Host SMTP", "smtp_host": "Host SMTP",
"smtp_port": "Porta SMTP", "smtp_port": "Porta SMTP",
@@ -177,21 +177,21 @@
"smtp_tls_option": "Opção SMTP TLS", "smtp_tls_option": "Opção SMTP TLS",
"email_tls_option": "Opção TLS para e-mail", "email_tls_option": "Opção TLS para e-mail",
"skip_certificate_verification": "Pular a verificação do certificado", "skip_certificate_verification": "Pular a verificação do certificado",
"this_can_be_useful_for_selfsigned_certificates": "Isso pode ser útil para certificados autoassinados.", "this_can_be_useful_for_selfsigned_certificates": "Isto pode ser útil para certificados autoassinados.",
"enabled_emails": "E-mails ativados", "enabled_emails": "E-mails ativados",
"email_login_notification": "Notificação de login por e-mail", "email_login_notification": "Notificação de login por e-mail",
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Manda um e-mail pro usuário quando ele entrar com um novo aparelho.", "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Envia um e-mail para o usuário quando ele entrar com um novo dispositivo.",
"emai_login_code_requested_by_user": "Código de login por e-mail solicitado pelo usuário", "emai_login_code_requested_by_user": "Código de login por e-mail solicitado pelo usuário",
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permite que os usuários pulem as senhas pedindo um código de login que vai pro e-mail deles. Isso deixa a segurança bem mais fraca, já que qualquer um que tiver acesso ao e-mail do usuário pode entrar.", "allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permite que os usuários ignorem suas chaves de acesso, pedindo um código de login diretamente no e-mail do usuário. Isso diminui a segurança, já que qualquer um com acesso ao e-mail do usuário pode entrar em sua conta.",
"email_login_code_from_admin": "Código de login por e-mail do administrador", "email_login_code_from_admin": "Código de login por e-mail do administrador",
"allows_an_admin_to_send_a_login_code_to_the_user": "Permite que um administrador envie um código de login pro usuário por e-mail.", "allows_an_admin_to_send_a_login_code_to_the_user": "Permite que um administrador envie um código de login para o e-mail do usuário.",
"send_test_email": "Enviar e-mail de teste", "send_test_email": "Enviar e-mail de teste",
"application_configuration_updated_successfully": "A configuração do aplicativo foi atualizada com sucesso.", "application_configuration_updated_successfully": "A configuração do aplicativo foi atualizada com sucesso.",
"application_name": "Nome do aplicativo", "application_name": "Nome do aplicativo",
"session_duration": "Duração da sessão", "session_duration": "Duração da sessão",
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "O tempo que dura uma sessão, em minutos, antes que o usuário precise fazer login de novo.", "the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "O tempo que dura uma sessão, em minutos, antes que o usuário precise fazer login de novo.",
"enable_self_account_editing": "Ativar edição da conta pessoal", "enable_self_account_editing": "Ativar edição da conta pessoal",
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Se os usuários podem editar os detalhes da conta deles.", "whether_the_users_should_be_able_to_edit_their_own_account_details": "Se os usuários podem editar os detalhes de suas contas.",
"emails_verified": "E-mails verificados", "emails_verified": "E-mails verificados",
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Se o e-mail do usuário deve ser marcado como verificado para os clientes OIDC.", "whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Se o e-mail do usuário deve ser marcado como verificado para os clientes OIDC.",
"ldap_configuration_updated_successfully": "Configuração LDAP atualizada com sucesso", "ldap_configuration_updated_successfully": "Configuração LDAP atualizada com sucesso",
@@ -199,13 +199,13 @@
"ldap_sync_finished": "Sincronização LDAP concluída", "ldap_sync_finished": "Sincronização LDAP concluída",
"client_configuration": "Configuração do cliente", "client_configuration": "Configuração do cliente",
"ldap_url": "URL LDAP", "ldap_url": "URL LDAP",
"ldap_bind_dn": "DN de ligação LDAP", "ldap_bind_dn": "LDAP vincular DN",
"ldap_bind_password": "Senha de ligação LDAP", "ldap_bind_password": "LDAP vincular Senha",
"ldap_base_dn": "DN base LDAP", "ldap_base_dn": "LDAP DN base",
"user_search_filter": "Filtro de pesquisa de usuários", "user_search_filter": "Filtro de pesquisa de usuários",
"the_search_filter_to_use_to_search_or_sync_users": "O filtro de pesquisa que você usa pra procurar/sincronizar usuários.", "the_search_filter_to_use_to_search_or_sync_users": "O filtro de pesquisa utilizado para encontrar/sincronizar usuários.",
"groups_search_filter": "Filtro de pesquisa de grupos", "groups_search_filter": "Filtro de pesquisa de grupos",
"the_search_filter_to_use_to_search_or_sync_groups": "O filtro de pesquisa que você usa pra procurar/sincronizar grupos.", "the_search_filter_to_use_to_search_or_sync_groups": "O filtro de pesquisa utilizado para encontrar/sincronizar grupos.",
"attribute_mapping": "Mapeamento de atributos", "attribute_mapping": "Mapeamento de atributos",
"user_unique_identifier_attribute": "Atributo de identificador único do usuário", "user_unique_identifier_attribute": "Atributo de identificador único do usuário",
"the_value_of_this_attribute_should_never_change": "O valor desse atributo nunca deve mudar.", "the_value_of_this_attribute_should_never_change": "O valor desse atributo nunca deve mudar.",
@@ -220,20 +220,20 @@
"group_unique_identifier_attribute": "Atributo identificador exclusivo do grupo", "group_unique_identifier_attribute": "Atributo identificador exclusivo do grupo",
"group_rdn_attribute": "Atributo RDN do grupo (em DN)", "group_rdn_attribute": "Atributo RDN do grupo (em DN)",
"admin_group_name": "Nome do grupo de administradores", "admin_group_name": "Nome do grupo de administradores",
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Os membros desse grupo vão ter privilégios de administrador no Pocket ID.", "members_of_this_group_will_have_admin_privileges_in_pocketid": "Os membros desse grupo terão privilégios de administrador no Pocket ID.",
"disable": "Desativar", "disable": "Desativar",
"sync_now": "Sincronizar agora", "sync_now": "Sincronizar agora",
"enable": "Ativar", "enable": "Ativar",
"user_created_successfully": "Usuário criado com sucesso", "user_created_successfully": "Usuário criado com sucesso",
"create_user": "Criar Usuário", "create_user": "Criar Usuário",
"add_a_new_user_to_appname": "Adicionar um novo usuário para {appName}", "add_a_new_user_to_appname": "Adicionar um novo usuário em {appName}",
"add_user": "Adicionar Usuário", "add_user": "Adicionar Usuário",
"manage_users": "Gerenciar Usuários", "manage_users": "Gerenciar Usuários",
"admin_privileges": "Privilégios de administrador", "admin_privileges": "Privilégios de administrador",
"admins_have_full_access_to_the_admin_panel": "Os administradores têm acesso total ao painel de administração.", "admins_have_full_access_to_the_admin_panel": "Os administradores têm acesso total ao painel de administração.",
"delete_firstname_lastname": "Apagar {firstName} {lastName}", "delete_firstname_lastname": "Deletar {firstName} {lastName}",
"are_you_sure_you_want_to_delete_this_user": "Tem certeza que quer excluir esse usuário?", "are_you_sure_you_want_to_delete_this_user": "Tem certeza que deseja deletar este usuário?",
"user_deleted_successfully": "Usuário excluído com sucesso", "user_deleted_successfully": "Usuário deletado com sucesso",
"role": "Função", "role": "Função",
"source": "Fonte", "source": "Fonte",
"admin": "Admin", "admin": "Admin",
@@ -243,12 +243,12 @@
"edit": "Editar", "edit": "Editar",
"user_groups_updated_successfully": "Grupos de usuários atualizados com sucesso", "user_groups_updated_successfully": "Grupos de usuários atualizados com sucesso",
"user_updated_successfully": "Usuário atualizado com sucesso", "user_updated_successfully": "Usuário atualizado com sucesso",
"custom_claims_updated_successfully": "Reclamações personalizadas atualizadas com sucesso", "custom_claims_updated_successfully": "Reivindicações personalizadas atualizadas com sucesso",
"back": "Voltar", "back": "Voltar",
"user_details_firstname_lastname": "Detalhes do usuário {firstName} {lastName}", "user_details_firstname_lastname": "Detalhes do usuário {firstName} {lastName}",
"manage_which_groups_this_user_belongs_to": "Controle quais grupos esse usuário faz parte.", "manage_which_groups_this_user_belongs_to": "Gerencie os grupos deste usuário.",
"custom_claims": "Reclamações personalizadas", "custom_claims": "Reivindicações personalizadas",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "As reivindicações personalizadas são pares de chave-valor que podem ser usados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “perfil” for solicitado.", "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "As reivindicações personalizadas são pares de chave-valor que podem ser utilizados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “profile” for solicitado.",
"user_group_created_successfully": "Grupo de usuários criado com sucesso", "user_group_created_successfully": "Grupo de usuários criado com sucesso",
"create_user_group": "Criar Grupo de Usuários", "create_user_group": "Criar Grupo de Usuários",
"create_a_new_group_that_can_be_assigned_to_users": "Crie um novo grupo que possa ser atribuído aos usuários.", "create_a_new_group_that_can_be_assigned_to_users": "Crie um novo grupo que possa ser atribuído aos usuários.",
@@ -256,38 +256,38 @@
"manage_user_groups": "Gerenciar grupos de usuários", "manage_user_groups": "Gerenciar grupos de usuários",
"friendly_name": "Nome Amigável", "friendly_name": "Nome Amigável",
"name_that_will_be_displayed_in_the_ui": "Nome que vai aparecer na interface do usuário", "name_that_will_be_displayed_in_the_ui": "Nome que vai aparecer na interface do usuário",
"name_that_will_be_in_the_groups_claim": "Nome que vai aparecer na reivindicação “grupos”", "name_that_will_be_in_the_groups_claim": "Nome que vai aparecer na reivindicação “groups”",
"delete_name": "Apagar {name}", "delete_name": "Deletar {name}",
"are_you_sure_you_want_to_delete_this_user_group": "Tem certeza que quer excluir esse grupo de usuários?", "are_you_sure_you_want_to_delete_this_user_group": "Tem certeza que deseja deletar este grupo de usuários?",
"user_group_deleted_successfully": "Grupo de usuários excluído com sucesso", "user_group_deleted_successfully": "Grupo de usuários deletado com sucesso",
"user_count": "Contagem de usuários", "user_count": "Total de usuários",
"user_group_updated_successfully": "Grupo de usuários atualizado com sucesso", "user_group_updated_successfully": "Grupo de usuários atualizado com sucesso",
"users_updated_successfully": "Usuários atualizados com sucesso", "users_updated_successfully": "Usuários atualizados com sucesso",
"user_group_details_name": "Detalhes do grupo de usuários {name}", "user_group_details_name": "Detalhes do grupo de usuários {name}",
"assign_users_to_this_group": "Adicione usuários a este grupo.", "assign_users_to_this_group": "Adicione usuários a este grupo.",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "As reivindicações personalizadas são pares de chave-valor que podem ser usados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “perfil” for solicitado. As reivindicações personalizadas definidas no usuário serão priorizadas se houver conflitos.", "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "As reivindicações personalizadas são pares de chave-valor que podem ser utilizados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “profile” for solicitado. As reivindicações personalizadas definidas no usuário serão priorizadas se houver conflitos.",
"oidc_client_created_successfully": "Cliente OIDC criado com sucesso", "oidc_client_created_successfully": "Cliente OIDC criado com sucesso",
"create_oidc_client": "Criar cliente OIDC", "create_oidc_client": "Criar cliente OIDC",
"add_a_new_oidc_client_to_appname": "Adicione um novo cliente OIDC em {appName}.", "add_a_new_oidc_client_to_appname": "Adicione um novo cliente OIDC em {appName}.",
"add_oidc_client": "Adicionar cliente OIDC", "add_oidc_client": "Adicionar cliente OIDC",
"manage_oidc_clients": "Gerenciar clientes OIDC", "manage_oidc_clients": "Gerenciar clientes OIDC",
"one_time_link": "Link único", "one_time_link": "Link de uso único",
"use_this_link_to_sign_in_once": "Use este link para fazer login uma vez. Isso é necessário para usuários que ainda não adicionaram uma chave de acesso ou que a perderam.", "use_this_link_to_sign_in_once": "Use este link para fazer login uma vez. Isso é necessário para usuários que ainda não adicionaram uma chave de acesso ou que a perderam.",
"add": "Adicionar", "add": "Adicionar",
"callback_urls": "URLs de retorno de chamada", "callback_urls": "URLs de retorno de chamada",
"logout_callback_urls": "URLs de retorno de chamada de logout", "logout_callback_urls": "URLs de retorno de chamada de logout",
"public_client": "Cliente Público", "public_client": "Cliente Público",
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.", "public_clients_description": "Os clientes públicos não têm um segredo de cliente. Eles são feitos para aplicativos móveis, web e nativos, onde os segredos não podem ser guardados com segurança.",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "A troca de chaves públicas é um recurso de segurança que evita ataques CSRF e interceptação de códigos de autorização.", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "A troca de chaves públicas é um recurso de segurança que evita ataques CSRF e interceptação de códigos de autorização.",
"requires_reauthentication": "Precisa autenticar de novo", "requires_reauthentication": "Obrigatório autenticar novamente",
"requires_users_to_authenticate_again_on_each_authorization": "Pede que os usuários se autentiquem de novo em cada autorização, mesmo que já estejam conectados.", "requires_users_to_authenticate_again_on_each_authorization": "Pede que os usuários se autentiquem novamente em cada autorização, mesmo que já estejam conectados",
"name_logo": "{name} logotipo", "name_logo": "{name} logotipo",
"change_logo": "Alterar logotipo", "change_logo": "Alterar logotipo",
"upload_logo": "Carregar logotipo", "upload_logo": "Carregar logotipo",
"remove_logo": "Tirar o logotipo", "remove_logo": "Remover o logotipo",
"are_you_sure_you_want_to_delete_this_oidc_client": "Tem certeza que quer apagar esse cliente OIDC?", "are_you_sure_you_want_to_delete_this_oidc_client": "Tem certeza que deseja deletar este cliente OIDC?",
"oidc_client_deleted_successfully": "Cliente OIDC excluído com sucesso", "oidc_client_deleted_successfully": "Cliente OIDC deletado com sucesso",
"authorization_url": "URL de autorização", "authorization_url": "URL de autorização",
"oidc_discovery_url": "URL de descoberta OIDC", "oidc_discovery_url": "URL de descoberta OIDC",
"token_url": "URL do token", "token_url": "URL do token",
@@ -295,10 +295,10 @@
"logout_url": "URL de logout", "logout_url": "URL de logout",
"certificate_url": "URL do certificado", "certificate_url": "URL do certificado",
"enabled": "Habilitado", "enabled": "Habilitado",
"disabled": "Deficientes", "disabled": "Inativo",
"oidc_client_updated_successfully": "Cliente OIDC atualizado com sucesso", "oidc_client_updated_successfully": "Cliente OIDC atualizado com sucesso",
"create_new_client_secret": "Criar novo segredo do cliente", "create_new_client_secret": "Criar novo segredo do cliente",
"are_you_sure_you_want_to_create_a_new_client_secret": "Tem certeza que quer criar um novo segredo de cliente? O antigo vai ser invalidado.", "are_you_sure_you_want_to_create_a_new_client_secret": "Tem certeza que deseja criar um novo segredo de cliente? O antigo será invalidado.",
"generate": "Gerar", "generate": "Gerar",
"new_client_secret_created_successfully": "Novo segredo do cliente criado com sucesso", "new_client_secret_created_successfully": "Novo segredo do cliente criado com sucesso",
"allowed_user_groups_updated_successfully": "Grupos de usuários permitidos atualizados com sucesso", "allowed_user_groups_updated_successfully": "Grupos de usuários permitidos atualizados com sucesso",
@@ -307,19 +307,19 @@
"client_secret": "Segredo do cliente", "client_secret": "Segredo do cliente",
"show_more_details": "Mostrar mais detalhes", "show_more_details": "Mostrar mais detalhes",
"allowed_user_groups": "Grupos de usuários permitidos", "allowed_user_groups": "Grupos de usuários permitidos",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Adicione grupos de usuários a este cliente para restringir o acesso aos usuários desses grupos. Se nenhum grupo de usuários for selecionado, todos os usuários terão acesso a este cliente.", "add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Adicione grupos de usuários a este cliente para restringir o acesso somente aos usuários que fazem parte destes grupos. Se nenhum grupo de usuários for selecionado, todos os usuários terão acesso a este cliente.",
"favicon": "Favicon", "favicon": "Ícone de Favorito",
"light_mode_logo": "Logotipo do modo claro", "light_mode_logo": "Logotipo do modo claro",
"dark_mode_logo": "Logotipo do Modo Escuro", "dark_mode_logo": "Logotipo do Modo Escuro",
"background_image": "Imagem de fundo", "background_image": "Imagem de fundo",
"language": "Idioma", "language": "Idioma",
"reset_profile_picture_question": "Queres redefinir a tua foto de perfil?", "reset_profile_picture_question": "Redefinir a foto de perfil?",
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Isso vai tirar a foto que você mandou e voltar a foto do perfil pro padrão. Quer mesmo continuar?", "this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Isto irá remover a foto atual e a imagem de perfil padrão será utilizada. Deseja continuar?",
"reset": "Redefinir", "reset": "Redefinir",
"reset_to_default": "Redefinir para o padrão", "reset_to_default": "Redefinir para o padrão",
"profile_picture_has_been_reset": "A foto do perfil foi redefinida. A atualização pode demorar alguns minutos.", "profile_picture_has_been_reset": "A foto do perfil foi redefinida. A atualização pode demorar alguns minutos.",
"select_the_language_you_want_to_use": "Escolha o idioma que você quer usar. Lembre-se de que alguns textos podem ser traduzidos automaticamente e podem não estar certos.", "select_the_language_you_want_to_use": "Escolha o idioma que deseja usar. Lembre-se de que alguns textos podem ser traduzidos automaticamente e podem não estar certos.",
"contribute_to_translation": "Se você encontrar algum problema, pode ajudar na tradução no <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.", "contribute_to_translation": "Se encontrar algum problema, ajude na tradução no <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
"personal": "Pessoal", "personal": "Pessoal",
"global": "Global", "global": "Global",
"all_users": "Todos os usuários", "all_users": "Todos os usuários",
@@ -327,42 +327,42 @@
"all_clients": "Todos os clientes", "all_clients": "Todos os clientes",
"all_locations": "Todos os locais", "all_locations": "Todos os locais",
"global_audit_log": "Registro de auditoria global", "global_audit_log": "Registro de auditoria global",
"see_all_account_activities_from_the_last_3_months": "Dá uma olhada em tudo que os usuários fizeram nos últimos 3 meses.", "see_all_account_activities_from_the_last_3_months": "Veja todas as atividades de todos os usuários nos últimos 3 meses.",
"token_sign_in": "Entrar com token", "token_sign_in": "Entrar com token",
"client_authorization": "Autorização do cliente", "client_authorization": "Autorização do cliente",
"new_client_authorization": "Autorização de novo cliente", "new_client_authorization": "Autorização de novo cliente",
"disable_animations": "Desativar animações", "disable_animations": "Desativar animações",
"turn_off_ui_animations": "Desligue as animações em toda a interface do usuário.", "turn_off_ui_animations": "Desligue as animações em toda a interface do usuário.",
"user_disabled": "Conta desativada", "user_disabled": "Conta desativada",
"disabled_users_cannot_log_in_or_use_services": "Usuários com deficiência não conseguem fazer login ou usar os serviços.", "disabled_users_cannot_log_in_or_use_services": "Usuários inativos não conseguem fazer login ou usar os serviços.",
"user_disabled_successfully": "O usuário foi desativado com sucesso.", "user_disabled_successfully": "O usuário foi desativado com sucesso.",
"user_enabled_successfully": "O usuário foi ativado com sucesso.", "user_enabled_successfully": "O usuário foi ativado com sucesso.",
"status": "Status", "status": "Status",
"disable_firstname_lastname": "Desativar {firstName} {lastName}", "disable_firstname_lastname": "Desativar {firstName} {lastName}",
"are_you_sure_you_want_to_disable_this_user": "Tem certeza que quer desativar esse usuário? Ele não vai conseguir entrar nem acessar nenhum serviço.", "are_you_sure_you_want_to_disable_this_user": "Tem certeza que deseja desativar este usuário? Não será mais possível entrar nem acessar os serviços.",
"ldap_soft_delete_users": "Impedir que usuários desativados acessem o LDAP.", "ldap_soft_delete_users": "Manter usuários desativados no LDAP.",
"ldap_soft_delete_users_description": "Quando ativada, os usuários removidos do LDAP serão desativados em vez de excluídos do sistema.", "ldap_soft_delete_users_description": "Quando ativado, usuários removidos do LDAP serão apenas desativados ao invés de deletados do sistema.",
"login_code_email_success": "O código de login foi enviado para o usuário.", "login_code_email_success": "O código de login foi enviado para o usuário.",
"send_email": "Enviar e-mail", "send_email": "Enviar e-mail",
"show_code": "Mostrar código", "show_code": "Mostrar código",
"callback_url_description": "URL(s) fornecido(s) pelo seu cliente. Vai ser adicionado automaticamente se você deixar em branco. Caracteres curinga (*) são aceitos, mas é melhor evitar para garantir mais segurança.", "callback_url_description": "URL(s) providenciados pelo cliente. Será adicionado automaticamente se deixar em branco. Apesar de caracteres curinga (*) serem aceitos, para uma segurança maior, não é recomendado utiliza-los.",
"logout_callback_url_description": "URL(s) que seu cliente deu pra sair da conta. Você pode usar curingas (*), mas é melhor evitar pra ficar mais seguro.", "logout_callback_url_description": "URL(s) providenciados pelo cliente para sair da conta. Apesar de curingas (*) serem aceitos, para uma segurança maior, não é recomendado utiliza-los.",
"api_key_expiration": "Expiração da chave API", "api_key_expiration": "Expiração da chave API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Manda um e-mail pro usuário quando a chave API dele estiver quase a expirar.", "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envia um e-mail para o usuário quando a chave API estiver próxima de expirar.",
"authorize_device": "Autorizar dispositivo", "authorize_device": "Autorizar dispositivo",
"the_device_has_been_authorized": "O dispositivo foi autorizado.", "the_device_has_been_authorized": "O dispositivo foi autorizado.",
"enter_code_displayed_in_previous_step": "Digite o código que apareceu na etapa anterior.", "enter_code_displayed_in_previous_step": "Digite o código que apareceu na etapa anterior.",
"authorize": "Autorizar", "authorize": "Autorizar",
"federated_client_credentials": "Credenciais de Cliente Federadas", "federated_client_credentials": "Credenciais de Cliente Federadas",
"federated_client_credentials_description": "Usando credenciais de cliente federadas, você pode autenticar clientes OIDC usando tokens JWT emitidos por autoridades de terceiros.", "federated_client_credentials_description": "Ao utilizar credenciais de cliente federadas, é possível autenticar clientes OIDC usando tokens JWT emitidos por autoridades de terceiros.",
"add_federated_client_credential": "Adicionar credencial de cliente federado", "add_federated_client_credential": "Adicionar credencial de cliente federado",
"add_another_federated_client_credential": "Adicionar outra credencial de cliente federado", "add_another_federated_client_credential": "Adicionar outra credencial de cliente federado",
"oidc_allowed_group_count": "Contagem de grupos permitidos", "oidc_allowed_group_count": "Total de grupos permitidos",
"unrestricted": "Sem restrições", "unrestricted": "Sem restrições",
"show_advanced_options": "Mostrar opções avançadas", "show_advanced_options": "Mostrar opções avançadas",
"hide_advanced_options": "Ocultar opções avançadas", "hide_advanced_options": "Ocultar opções avançadas",
"oidc_data_preview": "Pré-visualização dos dados OIDC", "oidc_data_preview": "Pré-visualização dos dados OIDC",
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Dá uma olhada nos dados OIDC que seriam enviados para diferentes usuários", "preview_the_oidc_data_that_would_be_sent_for_different_users": "Veja quais os dados OIDC que seriam enviados por diferentes usuários",
"id_token": "Token de identificação", "id_token": "Token de identificação",
"access_token": "Token de acesso", "access_token": "Token de acesso",
"userinfo": "Informações do usuário", "userinfo": "Informações do usuário",
@@ -370,41 +370,41 @@
"access_token_payload": "Carga útil do token de acesso", "access_token_payload": "Carga útil do token de acesso",
"userinfo_endpoint_response": "Resposta do ponto final da informação do usuário", "userinfo_endpoint_response": "Resposta do ponto final da informação do usuário",
"copy": "Copiar", "copy": "Copiar",
"no_preview_data_available": "Não tem dados de pré-visualização disponíveis", "no_preview_data_available": "Não dados de pré-visualização disponíveis",
"copy_all": "Copiar tudo", "copy_all": "Copiar tudo",
"preview": "Pré-visualização", "preview": "Pré-visualização",
"preview_for_user": "Prévia de “ {name} ”", "preview_for_user": "Prévia de “ {name} ”",
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Dá uma olhada nos dados OIDC que seriam enviados para esse usuário.", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Veja os dados OIDC que seriam enviados para este usuário",
"show": "Mostrar", "show": "Mostrar",
"select_an_option": "Escolha uma opção", "select_an_option": "Selecione uma opção",
"select_user": "Selecionar usuário", "select_user": "Selecionar usuário",
"error": "Erro", "error": "Erro",
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Escolha uma cor de destaque pra personalizar a aparência do Pocket ID.", "select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Escolha uma cor de destaque para personalizar a aparência do Pocket ID.",
"accent_color": "Cor de destaque", "accent_color": "Cor de destaque",
"custom_accent_color": "Cor de destaque personalizada", "custom_accent_color": "Cor de destaque personalizada",
"custom_accent_color_description": "Digite uma cor personalizada usando formatos de cor CSS válidos (por exemplo, hex, rgb, hsl).", "custom_accent_color_description": "Digite uma cor personalizada usando formatos de cor CSS válidos (por exemplo, hex, rgb, hsl).",
"color_value": "Valor da cor", "color_value": "Valor da cor",
"apply": "Inscreva-se", "apply": "Aplicar",
"signup_token": "Token de inscrição", "signup_token": "Token de inscrição",
"create_a_signup_token_to_allow_new_user_registration": "Crie um token de inscrição para permitir o registro de novos usuários.", "create_a_signup_token_to_allow_new_user_registration": "Crie um token de inscrição para permitir o registro de novos usuários.",
"usage_limit": "Limite de uso", "usage_limit": "Limite de uso",
"number_of_times_token_can_be_used": "Número de vezes que o token de inscrição pode ser usado.", "number_of_times_token_can_be_used": "Número de vezes que o token de inscrição pode ser usado.",
"expires": "Vence", "expires": "Valido até",
"signup": "Cadastre-se", "signup": "Cadastre-se",
"user_creation": "Criação de usuário", "user_creation": "Criar usuário",
"configure_user_creation": "Gerencie as configurações de criação de usuários, incluindo métodos de inscrição e permissões padrão para novos usuários.", "configure_user_creation": "Gerencie as configurações da criação de usuários, incluindo métodos de inscrição e permissões padrão para novos usuários.",
"user_creation_groups_description": "Atribuir esses grupos automaticamente aos novos usuários quando eles se cadastrarem.", "user_creation_groups_description": "Atribuir esses grupos automaticamente aos novos usuários no momento da inscrição.",
"user_creation_claims_description": "Atribua essas reivindicações personalizadas automaticamente aos novos usuários no momento da inscrição.", "user_creation_claims_description": "Atribua essas reivindicações personalizadas automaticamente aos novos usuários no momento da inscrição.",
"user_creation_updated_successfully": "As configurações de criação do usuário foram atualizadas com sucesso.", "user_creation_updated_successfully": "As configurações de criação do usuário foram atualizadas com sucesso.",
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.", "signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.",
"signup_requires_valid_token": preciso um token de inscrição válido pra criar uma conta.", "signup_requires_valid_token": necessário um token de inscrição válido para criar uma conta",
"validating_signup_token": "Validando o token de inscrição", "validating_signup_token": "Validando o token de inscrição",
"go_to_login": "Vá para o login", "go_to_login": "Vá para o login",
"signup_to_appname": "Cadastre-se em {appName}", "signup_to_appname": "Cadastre-se em {appName}",
"create_your_account_to_get_started": "Crie sua conta pra começar.", "create_your_account_to_get_started": "Crie sua conta para começar.",
"initial_account_creation_description": "Crie sua conta pra começar. Você vai poder definir uma senha mais tarde.", "initial_account_creation_description": "Crie sua conta para começar. Será possível cadastrar uma chave de acesso mais tarde.",
"setup_your_passkey": "Configure sua chave de acesso", "setup_your_passkey": "Configure sua chave de acesso",
"create_a_passkey_to_securely_access_your_account": "Crie uma senha para acessar sua conta com segurança. Essa vai ser sua principal forma de entrar.", "create_a_passkey_to_securely_access_your_account": "Cadastre uma chave de acesso para entrar em sua conta com segurança. Esta será sua principal forma de entrar.",
"skip_for_now": "Pular por enquanto", "skip_for_now": "Pular por enquanto",
"account_created": "Conta criada", "account_created": "Conta criada",
"enable_user_signups": "Ativar inscrições de usuários", "enable_user_signups": "Ativar inscrições de usuários",
@@ -423,27 +423,27 @@
"token": "Token", "token": "Token",
"loading": "Carregando", "loading": "Carregando",
"delete_signup_token": "Apagar token de inscrição", "delete_signup_token": "Apagar token de inscrição",
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que quer apagar esse token de inscrição? Não dá pra voltar atrás.", "are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que deseja deletar este token de inscrição? Não é possível desfazer esta ação.",
"signup_with_token": "Cadastre-se com token", "signup_with_token": "Cadastre-se com token",
"signup_with_token_description": "Os usuários só podem se cadastrar usando um token de cadastro válido criado por um administrador.", "signup_with_token_description": "Os usuários só podem se cadastrar usando um token de cadastro válido criado por um administrador.",
"signup_open": "Inscrição aberta", "signup_open": "Inscrição aberta",
"signup_open_description": "Qualquer pessoa pode criar uma conta nova sem restrições.", "signup_open_description": "Qualquer pessoa pode criar uma conta nova, sem restrições.",
"of": "de", "of": "de",
"skip_passkey_setup": "Pular configuração da chave de acesso", "skip_passkey_setup": "Pular configuração da chave de acesso",
"skip_passkey_setup_description": super recomendável criar uma senha de acesso, porque sem ela você vai ficar sem poder entrar na sua conta assim que a sessão acabar.", "skip_passkey_setup_description": altamente recomendado cadastrar uma chave de acesso, sem uma, não se possível entrar em sua conta novamente quando esta sessão acabar.",
"my_apps": "Meus aplicativos", "my_apps": "Meus aplicativos",
"no_apps_available": "Não tem aplicativos disponíveis", "no_apps_available": "Não aplicativos disponíveis",
"contact_your_administrator_for_app_access": "Fala com o seu administrador pra conseguir acesso aos aplicativos.", "contact_your_administrator_for_app_access": "Fale com o seu administrador para conseguir acesso aos aplicativos.",
"launch": "Lançamento", "launch": "Lançamento",
"client_launch_url": "URL de lançamento do cliente", "client_launch_url": "URL de lançamento do cliente",
"client_launch_url_description": "A URL que vai abrir quando alguém abrir o aplicativo na página Meus aplicativos.", "client_launch_url_description": "A URL carregada quando um usuário abrir o aplicativo na página Meus aplicativos.",
"client_name_description": "O nome do cliente que aparece na interface do Pocket ID.", "client_name_description": "O nome do cliente que aparece na interface do Pocket ID.",
"revoke_access": "Revogar acesso", "revoke_access": "Revogar acesso",
"revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não vai mais conseguir acessar as informações da sua conta.", "revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não conseguirá mais acessar as informações da sua conta.",
"revoke_access_successful": "O acesso a {clientName} foi revogado com sucesso.", "revoke_access_successful": "O acesso a {clientName} foi revogado com sucesso.",
"last_signed_in_ago": "Último login em {time} atrás", "last_signed_in_ago": "Último login à {time} atrás",
"invalid_client_id": "A ID do cliente só pode ter letras, números, sublinhados e hífens.", "invalid_client_id": "A ID do cliente só pode ter letras, números, sub traços e hifens",
"custom_client_id_description": "Defina um ID de cliente personalizado se for necessário para o seu aplicativo. Caso contrário, deixe em branco para gerar um aleatório.", "custom_client_id_description": "Se requerido pelo seu aplicativo, defina um ID de cliente personalizado. Caso contrário, deixe em branco para gerar um aleatório.",
"generated": "Gerado", "generated": "Gerado",
"administration": "Administração", "administration": "Administração",
"group_rdn_attribute_description": "O atributo usado no nome distinto (DN) dos grupos.", "group_rdn_attribute_description": "O atributo usado no nome distinto (DN) dos grupos.",
@@ -455,5 +455,11 @@
"logo_from_url_description": "Cole uma URL direta da imagem (svg, png, webp). Encontre ícones em <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Cole uma URL direta da imagem (svg, png, webp). Encontre ícones em <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "URL inválido", "invalid_url": "URL inválido",
"require_user_email": "É preciso um endereço de e-mail", "require_user_email": "É preciso um endereço de e-mail",
"require_user_email_description": "Pede que os usuários tenham um endereço de e-mail. Se desativado, os usuários sem endereço de e-mail não vão poder usar os recursos que precisam disso." "require_user_email_description": "Obriga que os usuários tenham um endereço de e-mail. Se desativado, os usuários sem endereço de e-mail não vão poder usar os recursos que precisam disso.",
"view": "Ver",
"toggle_columns": "Alternar colunas",
"locale": "Localização",
"ldap_id": "ID LDAP",
"reauthentication": "Re-autenticação",
"clear_filters": "Limpar filtros"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Вставьте прямой URL-адрес изображения (svg, png, webp). Ищите иконки на <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> или <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Вставьте прямой URL-адрес изображения (svg, png, webp). Ищите иконки на <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> или <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Недопустимый URL", "invalid_url": "Недопустимый URL",
"require_user_email": "Требовать адрес электронной почты", "require_user_email": "Требовать адрес электронной почты",
"require_user_email_description": "Требует, чтобы у пользователей был адрес электронной почты. Если эта функция отключена, пользователи без адреса электронной почты не смогут пользоваться функциями, для которых нужен адрес электронной почты." "require_user_email_description": "Требует, чтобы у пользователей был адрес электронной почты. Если эта функция отключена, пользователи без адреса электронной почты не смогут пользоваться функциями, для которых нужен адрес электронной почты.",
"view": "Посмотреть",
"toggle_columns": "Переключать столбцы",
"locale": "Локаль",
"ldap_id": "LDAP-идентификатор",
"reauthentication": "Повторная аутентификация",
"clear_filters": "Очистить фильтры"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Klistra in en direkt bild-URL (svg, png, webp). Hitta ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Klistra in en direkt bild-URL (svg, png, webp). Hitta ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Ogiltig URL", "invalid_url": "Ogiltig URL",
"require_user_email": "Kräver e-postadress", "require_user_email": "Kräver e-postadress",
"require_user_email_description": "Kräver att användarna har en e-postadress. Om funktionen är inaktiverad kan användare utan e-postadress inte använda funktioner som kräver en e-postadress." "require_user_email_description": "Kräver att användarna har en e-postadress. Om funktionen är inaktiverad kan användare utan e-postadress inte använda funktioner som kräver en e-postadress.",
"view": "Visa",
"toggle_columns": "Växla kolumner",
"locale": "Lokalisering",
"ldap_id": "LDAP-ID",
"reauthentication": "Omverifiering",
"clear_filters": "Rensa filter"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Вставте прямий URL-адресу зображення (svg, png, webp). Знайдіть іконки на <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> або <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Вставте прямий URL-адресу зображення (svg, png, webp). Знайдіть іконки на <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> або <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "Недійсний URL-адреса", "invalid_url": "Недійсний URL-адреса",
"require_user_email": "Потрібна адреса електронної пошти", "require_user_email": "Потрібна адреса електронної пошти",
"require_user_email_description": "Вимагає від користувачів наявність адреси електронної пошти. Якщо ця опція вимкнена, користувачі без адреси електронної пошти не зможуть користуватися функціями, для яких потрібна адреса електронної пошти." "require_user_email_description": "Вимагає від користувачів наявність адреси електронної пошти. Якщо ця опція вимкнена, користувачі без адреси електронної пошти не зможуть користуватися функціями, для яких потрібна адреса електронної пошти.",
"view": "Перегляд",
"toggle_columns": "Перемикання стовпців",
"locale": "Локаль",
"ldap_id": "LDAP-ідентифікатор",
"reauthentication": "Повторна аутентифікація",
"clear_filters": "Очистити фільтри"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "Dán URL hình ảnh trực tiếp (svg, png, webp). Tìm biểu tượng tại <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> hoặc <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.", "logo_from_url_description": "Dán URL hình ảnh trực tiếp (svg, png, webp). Tìm biểu tượng tại <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> hoặc <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
"invalid_url": "URL không hợp lệ", "invalid_url": "URL không hợp lệ",
"require_user_email": "Yêu cầu địa chỉ email", "require_user_email": "Yêu cầu địa chỉ email",
"require_user_email_description": "Yêu cầu người dùng phải có địa chỉ email. Nếu tính năng này bị vô hiệu hóa, những người dùng không có địa chỉ email sẽ không thể sử dụng các tính năng yêu cầu địa chỉ email." "require_user_email_description": "Yêu cầu người dùng phải có địa chỉ email. Nếu tính năng này bị vô hiệu hóa, những người dùng không có địa chỉ email sẽ không thể sử dụng các tính năng yêu cầu địa chỉ email.",
"view": "Xem",
"toggle_columns": "Bật/tắt cột",
"locale": "Vùng",
"ldap_id": "ID LDAP",
"reauthentication": "Xác thực lại",
"clear_filters": "Xóa bộ lọc"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "粘贴直接图片URLsvg、png、webp格式。<link href=\"https://selfh.st/icons\">可在Selfh.st图标库</link>或<link href=\"https://dashboardicons.com\">仪表盘图标库</link>中查找图标。", "logo_from_url_description": "粘贴直接图片URLsvg、png、webp格式。<link href=\"https://selfh.st/icons\">可在Selfh.st图标库</link>或<link href=\"https://dashboardicons.com\">仪表盘图标库</link>中查找图标。",
"invalid_url": "无效网址", "invalid_url": "无效网址",
"require_user_email": "需要电子邮件地址", "require_user_email": "需要电子邮件地址",
"require_user_email_description": "要求用户拥有电子邮件地址。若禁用此功能,没有电子邮件地址的用户将无法使用需要电子邮件地址的功能。" "require_user_email_description": "要求用户拥有电子邮件地址。若禁用此功能,没有电子邮件地址的用户将无法使用需要电子邮件地址的功能。",
"view": "查看",
"toggle_columns": "切换列",
"locale": "区域设置",
"ldap_id": "LDAP标识符",
"reauthentication": "重新认证",
"clear_filters": "清除筛选条件"
} }

View File

@@ -455,5 +455,11 @@
"logo_from_url_description": "貼上直接圖片網址svg、png、webp。在<link href=\"https://selfh.st/icons\">Selfh.st 圖示庫或</link> <link href=\"https://dashboardicons.com\">儀表板圖示庫中</link>尋找圖示。", "logo_from_url_description": "貼上直接圖片網址svg、png、webp。在<link href=\"https://selfh.st/icons\">Selfh.st 圖示庫或</link> <link href=\"https://dashboardicons.com\">儀表板圖示庫中</link>尋找圖示。",
"invalid_url": "無效網址", "invalid_url": "無效網址",
"require_user_email": "需要電子郵件地址", "require_user_email": "需要電子郵件地址",
"require_user_email_description": "要求使用者必須擁有電子郵件地址。若此功能被停用,沒有電子郵件地址的使用者將無法使用需要電子郵件地址的功能。" "require_user_email_description": "要求使用者必須擁有電子郵件地址。若此功能被停用,沒有電子郵件地址的使用者將無法使用需要電子郵件地址的功能。",
"view": "檢視",
"toggle_columns": "切換欄位",
"locale": "地區",
"ldap_id": "LDAP 識別碼",
"reauthentication": "重新驗證",
"clear_filters": "清除篩選條件"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "1.13.0", "version": "1.14.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -14,50 +14,50 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/browser": "^13.2.2",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.12.0", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"jose": "^5.10.0", "jose": "^5.10.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"runed": "^0.31.1", "runed": "^0.31.1",
"sveltekit-superforms": "^2.27.1", "sveltekit-superforms": "^2.28.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"zod": "^4.0.9" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@inlang/paraglide-js": "^2.2.0", "@inlang/paraglide-js": "^2.4.0",
"@inlang/plugin-m-function-matcher": "^2.1.0", "@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0", "@inlang/plugin-message-format": "^4.0.0",
"@internationalized/date": "^3.8.2", "@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.525.0", "@lucide/svelte": "^0.525.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.36.3", "@sveltejs/kit": "^2.47.3",
"@sveltejs/vite-plugin-svelte": "^6.1.0", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/node": "^22.16.5", "@types/node": "^22.18.12",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.6",
"bits-ui": "^2.8.11", "bits-ui": "^2.14.1",
"eslint": "^9.31.0", "eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.11.0", "eslint-plugin-svelte": "^3.12.5",
"formsnap": "^2.0.1", "formsnap": "^2.0.1",
"globals": "^16.3.0", "globals": "^16.4.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"rollup": "^4.46.3", "rollup": "^4.52.5",
"svelte": "^5.36.16", "svelte": "^5.41.3",
"svelte-check": "^4.3.0", "svelte-check": "^4.3.3",
"svelte-sonner": "^1.0.5", "svelte-sonner": "^1.0.5",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.16",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tw-animate-css": "^1.3.5", "tw-animate-css": "^1.4.0",
"typescript": "^5.8.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.38.0", "typescript-eslint": "^8.46.2",
"vite": "^7.0.7" "vite": "^7.1.12"
} }
} }

View File

@@ -1,217 +0,0 @@
<script lang="ts" generics="T extends {id:string}">
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import { Input } from '$lib/components/ui/input/index.js';
import * as Pagination from '$lib/components/ui/pagination';
import * as Select from '$lib/components/ui/select';
import * as Table from '$lib/components/ui/table/index.js';
import Empty from '$lib/icons/empty.svelte';
import { m } from '$lib/paraglide/messages';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import { debounced } from '$lib/utils/debounce-util';
import { cn } from '$lib/utils/style';
import { ChevronDown } from '@lucide/svelte';
import type { Snippet } from 'svelte';
import Button from './ui/button/button.svelte';
let {
items,
requestOptions = $bindable(),
selectedIds = $bindable(),
withoutSearch = false,
selectionDisabled = false,
onRefresh,
columns,
rows
}: {
items: Paginated<T>;
requestOptions: SearchPaginationSortRequest;
selectedIds?: string[];
withoutSearch?: boolean;
selectionDisabled?: boolean;
onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise<Paginated<T>>;
columns: { label: string; hidden?: boolean; sortColumn?: string }[];
rows: Snippet<[{ item: T }]>;
} = $props();
let searchValue = $state('');
let availablePageSizes: number[] = [20, 50, 100];
let allChecked = $derived.by(() => {
if (!selectedIds || items.data.length === 0) return false;
for (const item of items.data) {
if (!selectedIds.includes(item.id)) {
return false;
}
}
return true;
});
const onSearch = debounced(async (search: string) => {
requestOptions.search = search;
await onRefresh(requestOptions);
searchValue = search;
}, 300);
async function onAllCheck(checked: boolean) {
const pageIds = items.data.map((item) => item.id);
const current = selectedIds ?? [];
if (checked) {
selectedIds = Array.from(new Set([...current, ...pageIds]));
} else {
selectedIds = current.filter((id) => !pageIds.includes(id));
}
}
async function onCheck(checked: boolean, id: string) {
const current = selectedIds ?? [];
if (checked) {
selectedIds = Array.from(new Set([...current, id]));
} else {
selectedIds = current.filter((selectedId) => selectedId !== id);
}
}
async function onPageChange(page: number) {
requestOptions.pagination = { limit: items.pagination.itemsPerPage, page };
onRefresh(requestOptions);
}
async function onPageSizeChange(size: number) {
requestOptions.pagination = { limit: size, page: 1 };
onRefresh(requestOptions);
}
async function onSort(column?: string, direction: 'asc' | 'desc' = 'asc') {
if (!column) return;
requestOptions.sort = { column, direction };
onRefresh(requestOptions);
}
</script>
{#if !withoutSearch}
<Input
value={searchValue}
class={cn(
'relative z-50 mb-4 max-w-sm',
items.data.length == 0 && searchValue == '' && 'hidden'
)}
placeholder={m.search()}
type="text"
oninput={(e: Event) => onSearch((e.currentTarget as HTMLInputElement).value)}
/>
{/if}
{#if items.data.length === 0 && searchValue === ''}
<div class="my-5 flex flex-col items-center">
<Empty class="text-muted-foreground h-20" />
<p class="text-muted-foreground mt-3 text-sm">{m.no_items_found()}</p>
</div>
{:else}
<Table.Root class="min-w-full table-auto overflow-x-auto">
<Table.Header>
<Table.Row>
{#if selectedIds}
<Table.Head class="w-12">
<Checkbox
disabled={selectionDisabled}
checked={allChecked}
onCheckedChange={(c: boolean) => onAllCheck(c as boolean)}
/>
</Table.Head>
{/if}
{#each columns as column}
<Table.Head class={cn(column.hidden && 'sr-only', column.sortColumn && 'px-0')}>
{#if column.sortColumn}
<Button
variant="ghost"
class="flex items-center"
onclick={() =>
onSort(
column.sortColumn,
requestOptions.sort?.direction === 'desc' ? 'asc' : 'desc'
)}
>
{column.label}
{#if requestOptions.sort?.column === column.sortColumn}
<ChevronDown
class={cn(
'ml-2 size-4',
requestOptions.sort?.direction === 'asc' ? 'rotate-180' : ''
)}
/>
{/if}
</Button>
{:else}
{column.label}
{/if}
</Table.Head>
{/each}
</Table.Row>
</Table.Header>
<Table.Body>
{#each items.data as item}
<Table.Row class={selectedIds?.includes(item.id) ? 'bg-muted/20' : ''}>
{#if selectedIds}
<Table.Cell class="w-12">
<Checkbox
disabled={selectionDisabled}
checked={selectedIds.includes(item.id)}
onCheckedChange={(c: boolean) => onCheck(c, item.id)}
/>
</Table.Cell>
{/if}
{@render rows({ item })}
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
<div class="mt-5 flex flex-col-reverse items-center justify-between gap-3 sm:flex-row">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">{m.items_per_page()}</p>
<Select.Root
type="single"
value={items.pagination.itemsPerPage.toString()}
onValueChange={(v) => onPageSizeChange(Number(v))}
>
<Select.Trigger class="h-9 w-[80px]">
{items.pagination.itemsPerPage}
</Select.Trigger>
<Select.Content>
{#each availablePageSizes as size}
<Select.Item value={size.toString()}>{size}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Pagination.Root
class="mx-0 w-auto"
count={items.pagination.totalItems}
perPage={items.pagination.itemsPerPage}
{onPageChange}
page={items.pagination.currentPage}
>
{#snippet children({ pages })}
<Pagination.Content class="flex justify-end">
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type !== 'ellipsis' && page.value != 0}
<Pagination.Item>
<Pagination.Link {page} isActive={items.pagination.currentPage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
</div>
{/if}

View File

@@ -1,69 +1,111 @@
<script lang="ts"> <script lang="ts">
import AdvancedTable from '$lib/components/advanced-table.svelte'; import AdvancedTable from '$lib/components/table/advanced-table.svelte';
import { Badge } from '$lib/components/ui/badge'; import { Badge } from '$lib/components/ui/badge';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import {translateAuditLogEvent} from "$lib/utils/audit-log-translator";
import AuditLogService from '$lib/services/audit-log-service'; import AuditLogService from '$lib/services/audit-log-service';
import type { AuditLog } from '$lib/types/audit-log.type'; import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { AuditLog, AuditLogFilter } from '$lib/types/audit-log.type';
import { translateAuditLogEvent } from '$lib/utils/audit-log-translator';
let { let {
auditLogs,
isAdmin = false, isAdmin = false,
requestOptions filters
}: { }: {
auditLogs: Paginated<AuditLog>;
isAdmin?: boolean; isAdmin?: boolean;
requestOptions: SearchPaginationSortRequest; filters?: AuditLogFilter;
} = $props(); } = $props();
const auditLogService = new AuditLogService(); const auditLogService = new AuditLogService();
let tableRef: AdvancedTable<AuditLog>;
const columns: AdvancedTableColumn<AuditLog>[] = [
{
label: m.time(),
column: 'createdAt',
sortable: true,
value: (item) => new Date(item.createdAt).toLocaleString()
},
{
label: m.username(),
column: 'username',
hidden: !isAdmin,
value: (item) => item.username ?? m.unknown()
},
{
label: m.event(),
column: 'event',
sortable: true,
cell: EventCell
},
{
label: m.approximate_location(),
key: 'location',
value: (item) => formatLocation(item)
},
{
label: m.ip_address(),
column: 'ipAddress',
sortable: true
},
{
label: m.device(),
column: 'device',
sortable: true
},
{
label: m.client(),
key: 'client',
value: (item) => item.data?.clientName
}
];
$effect(() => {
if (filters) {
tableRef?.refresh();
}
});
export async function refresh() {
await tableRef.refresh();
}
function formatLocation(log: AuditLog) {
if (log.city && log.country) {
return `${log.city}, ${log.country}`;
} else if (log.country) {
return log.country;
} else {
return m.unknown();
}
}
function wrapFilters(filters?: Record<string, string>) {
if (!filters) return undefined;
return Object.fromEntries(
Object.entries(filters)
.filter(([_, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => [key, [value]])
);
}
</script> </script>
{#snippet EventCell({ item }: { item: AuditLog })}
<Badge class="rounded-full" variant="outline">
{translateAuditLogEvent(item.event)}
</Badge>
{/snippet}
<AdvancedTable <AdvancedTable
items={auditLogs} id="audit-log-list-{isAdmin ? 'admin' : 'user'}"
{requestOptions} bind:this={tableRef}
onRefresh={async (options) => fetchCallback={async (options) =>
isAdmin isAdmin
? (auditLogs = await auditLogService.listAllLogs(options)) ? await auditLogService.listAllLogs({
: (auditLogs = await auditLogService.list(options))} ...options,
columns={[ filters: wrapFilters(filters)
{ label: m.time(), sortColumn: 'createdAt' }, })
...(isAdmin ? [{ label: 'Username' }] : []), : await auditLogService.list(options)}
{ label: m.event(), sortColumn: 'event' }, defaultSort={{ column: 'createdAt', direction: 'desc' }}
{ label: m.approximate_location(), sortColumn: 'city' },
{ label: m.ip_address(), sortColumn: 'ipAddress' },
{ label: m.device(), sortColumn: 'device' },
{ label: m.client() }
]}
withoutSearch withoutSearch
> {columns}
{#snippet rows({ item })} />
<Table.Cell>{new Date(item.createdAt).toLocaleString()}</Table.Cell>
{#if isAdmin}
<Table.Cell>
{#if item.username}
{item.username}
{:else}
Unknown User
{/if}
</Table.Cell>
{/if}
<Table.Cell>
<Badge class="rounded-full" variant="outline">{translateAuditLogEvent(item.event)}</Badge>
</Table.Cell>
<Table.Cell>
{#if item.city && item.country}
{item.city}, {item.country}
{:else if item.country}
{item.country}
{:else}
{m.unknown()}
{/if}
</Table.Cell>
<Table.Cell>{item.ipAddress}</Table.Cell>
<Table.Cell>{item.device}</Table.Cell>
<Table.Cell>{item.data.clientName}</Table.Cell>
{/snippet}
</AdvancedTable>

View File

@@ -12,11 +12,13 @@
let { let {
label, label,
accept, accept,
onchange onchange,
id = 'file-input'
}: { }: {
label: string; label: string;
accept?: string; accept?: string;
onchange: (file: File | string | null) => void; onchange: (file: File | string | null) => void;
id?: string;
} = $props(); } = $props();
let url = $state(''); let url = $state('');
@@ -47,7 +49,7 @@
<div class="flex"> <div class="flex">
<FileInput <FileInput
id="logo" {id}
variant="secondary" variant="secondary"
{accept} {accept}
onchange={handleFileChange} onchange={handleFileChange}
@@ -64,9 +66,9 @@
<LucideChevronDown class="size-4" /></Popover.Trigger <LucideChevronDown class="size-4" /></Popover.Trigger
> >
<Popover.Content class="w-80"> <Popover.Content class="w-80">
<Label for="file-url" class="text-xs">URL</Label> <Label for="{id}-url" class="text-xs">URL</Label>
<Input <Input
id="file-url" id="{id}-url"
placeholder="" placeholder=""
value={url} value={url}
oninput={(e) => (url = e.currentTarget.value)} oninput={(e) => (url = e.currentTarget.value)}

View File

@@ -12,14 +12,10 @@
}); });
</script> </script>
<div class={'bg-muted flex items-center justify-center rounded-2xl p-3'}> <div class={cn('bg-muted flex items-center justify-center rounded-2xl p-3', props.class)}>
{#if error} {#if error}
<LucideImageOff class={cn('text-muted-foreground p-5', props.class)} /> <LucideImageOff class="text-muted-foreground p-5" />
{:else} {:else}
<img <img {...props} class="aspect-square object-contain" onerror={() => (error = true)} />
{...props}
class={cn('object-contain aspect-square', props.class)}
onerror={() => (error = true)}
/>
{/if} {/if}
</div> </div>

View File

@@ -1,23 +1,29 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
import { cachedBackgroundImage } from '$lib/utils/cached-image-util'; import { cachedBackgroundImage } from '$lib/utils/cached-image-util';
import { cn } from '$lib/utils/style'; import { cn } from '$lib/utils/style';
import type { Snippet } from 'svelte'; import { type Snippet } from 'svelte';
import { MediaQuery } from 'svelte/reactivity'; import { MediaQuery } from 'svelte/reactivity';
import * as Card from './ui/card'; import * as Card from './ui/card';
let { let {
children, children,
showAlternativeSignInMethodButton = false, showAlternativeSignInMethodButton = false
animate = false
}: { }: {
children: Snippet; children: Snippet;
showAlternativeSignInMethodButton?: boolean; showAlternativeSignInMethodButton?: boolean;
animate?: boolean;
} = $props(); } = $props();
let isInitialLoad = $state(false);
let animate = $derived(isInitialLoad && !$appConfigStore.disableAnimations);
afterNavigate((e) => {
isInitialLoad = !e?.from?.url;
});
const isDesktop = new MediaQuery('min-width: 1024px'); const isDesktop = new MediaQuery('min-width: 1024px');
let alternativeSignInButton = $state({ let alternativeSignInButton = $state({
href: '/login/alternative', href: '/login/alternative',

View File

@@ -1,33 +1,29 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import AdvancedTable from '$lib/components/advanced-table.svelte';
import { openConfirmDialog } from '$lib/components/confirm-dialog/'; import { openConfirmDialog } from '$lib/components/confirm-dialog/';
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
import { Badge, type BadgeVariant } from '$lib/components/ui/badge'; import { Badge, type BadgeVariant } from '$lib/components/ui/badge';
import { Button, buttonVariants } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type {
AdvancedTableColumn,
CreateAdvancedTableActions
} from '$lib/types/advanced-table.type';
import type { SignupTokenDto } from '$lib/types/signup-token.type'; import type { SignupTokenDto } from '$lib/types/signup-token.type';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { Copy, Ellipsis, Trash2 } from '@lucide/svelte'; import { Copy, Trash2 } from '@lucide/svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
let { let {
open = $bindable(), open = $bindable()
signupTokens = $bindable(),
signupTokensRequestOptions,
onTokenDeleted
}: { }: {
open: boolean; open: boolean;
signupTokens: Paginated<SignupTokenDto>;
signupTokensRequestOptions: SearchPaginationSortRequest;
onTokenDeleted?: () => Promise<void>;
} = $props(); } = $props();
const userService = new UserService(); const userService = new UserService();
let tableRef: AdvancedTable<SignupTokenDto>;
function formatDate(dateStr: string | undefined) { function formatDate(dateStr: string | undefined) {
if (!dateStr) return m.never(); if (!dateStr) return m.never();
@@ -44,12 +40,8 @@
action: async () => { action: async () => {
try { try {
await userService.deleteSignupToken(token.id); await userService.deleteSignupToken(token.id);
await tableRef.refresh();
toast.success(m.signup_token_deleted_successfully()); toast.success(m.signup_token_deleted_successfully());
// Refresh the tokens
if (onTokenDeleted) {
await onTokenDeleted();
}
} catch (e) { } catch (e) {
axiosErrorToast(e); axiosErrorToast(e);
} }
@@ -98,8 +90,69 @@
axiosErrorToast(err); axiosErrorToast(err);
}); });
} }
const columns: AdvancedTableColumn<SignupTokenDto>[] = [
{ label: m.token(), column: 'token', cell: TokenCell },
{ label: m.status(), key: 'status', cell: StatusCell },
{
label: m.usage(),
column: 'usageCount',
sortable: true,
cell: UsageCell
},
{
label: m.expires(),
column: 'expiresAt',
sortable: true,
value: (item) => formatDate(item.expiresAt)
},
{ label: 'Usage Limit', column: 'usageLimit' },
{
label: m.created(),
column: 'createdAt',
sortable: true,
hidden: true,
value: (item) => formatDate(item.createdAt)
}
];
const actions: CreateAdvancedTableActions<SignupTokenDto> = (_) => [
{
label: m.copy(),
icon: Copy,
onClick: (token) => copySignupLink(token)
},
{
label: m.delete(),
icon: Trash2,
variant: 'danger',
onClick: (token) => deleteToken(token)
}
];
</script> </script>
{#snippet TokenCell({ item }: { item: SignupTokenDto })}
<span class="font-mono text-xs">
{item.token.substring(0, 3)}...{item.token.substring(Math.max(item.token.length - 4, 0))}
</span>
{/snippet}
{#snippet StatusCell({ item }: { item: SignupTokenDto })}
{@const status = getTokenStatus(item)}
{@const statusBadge = getStatusBadge(status)}
<Badge class="rounded-full" variant={statusBadge.variant}>
{statusBadge.text}
</Badge>
{/snippet}
{#snippet UsageCell({ item }: { item: SignupTokenDto })}
<div class="flex items-center gap-1">
{item.usageCount}
{m.of()}
{item.usageLimit}
</div>
{/snippet}
<Dialog.Root {open} {onOpenChange}> <Dialog.Root {open} {onOpenChange}>
<Dialog.Content class="sm-min-w[500px] max-h-[90vh] min-w-[90vw] overflow-auto lg:min-w-[1000px]"> <Dialog.Content class="sm-min-w[500px] max-h-[90vh] min-w-[90vw] overflow-auto lg:min-w-[1000px]">
<Dialog.Header> <Dialog.Header>
@@ -111,70 +164,13 @@
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<AdvancedTable <AdvancedTable
items={signupTokens} id="signup-token-list"
requestOptions={signupTokensRequestOptions}
withoutSearch={true} withoutSearch={true}
onRefresh={async (options) => { fetchCallback={userService.listSignupTokens}
const result = await userService.listSignupTokens(options); bind:this={tableRef}
signupTokens = result; {columns}
return result; {actions}
}} />
columns={[
{ label: m.token() },
{ label: m.status() },
{ label: m.usage(), sortColumn: 'usageCount' },
{ label: m.expires(), sortColumn: 'expiresAt' },
{ label: m.created(), sortColumn: 'createdAt' },
{ label: m.actions(), hidden: true }
]}
>
{#snippet rows({ item })}
<Table.Cell class="font-mono text-xs">
{item.token.substring(0, 2)}...{item.token.substring(item.token.length - 4)}
</Table.Cell>
<Table.Cell>
{@const status = getTokenStatus(item)}
{@const statusBadge = getStatusBadge(status)}
<Badge class="rounded-full" variant={statusBadge.variant}>
{statusBadge.text}
</Badge>
</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-1">
{`${item.usageCount} ${m.of()} ${item.usageLimit}`}
</div>
</Table.Cell>
<Table.Cell class="text-sm">
<div class="flex items-center gap-1">
{formatDate(item.expiresAt)}
</div>
</Table.Cell>
<Table.Cell class="text-sm">
{formatDate(item.createdAt)}
</Table.Cell>
<Table.Cell>
<DropdownMenu.Root>
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
<Ellipsis class="size-4" />
<span class="sr-only">{m.toggle_menu()}</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={() => copySignupLink(item)}>
<Copy class="mr-2 size-4" />
{m.copy()}
</DropdownMenu.Item>
<DropdownMenu.Item
class="text-red-500 focus:!text-red-700"
onclick={() => deleteToken(item)}
>
<Trash2 class="mr-2 size-4" />
{m.delete()}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
{/snippet}
</AdvancedTable>
</div> </div>
<Dialog.Footer class="mt-3"> <Dialog.Footer class="mt-3">
<Button onclick={() => (open = false)}> <Button onclick={() => (open = false)}>

View File

@@ -13,11 +13,9 @@
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
let { let {
open = $bindable(), open = $bindable()
onTokenCreated
}: { }: {
open: boolean; open: boolean;
onTokenCreated?: () => Promise<void>;
} = $props(); } = $props();
const userService = new UserService(); const userService = new UserService();
@@ -37,12 +35,11 @@
async function createSignupToken() { async function createSignupToken() {
try { try {
signupToken = await userService.createSignupToken(availableExpirations[selectedExpiration], usageLimit); signupToken = await userService.createSignupToken(
availableExpirations[selectedExpiration],
usageLimit
);
signupLink = `${page.url.origin}/st/${signupToken}`; signupLink = `${page.url.origin}/st/${signupToken}`;
if (onTokenCreated) {
await onTokenCreated();
}
} catch (e) { } catch (e) {
axiosErrorToast(e); axiosErrorToast(e);
} }

View File

@@ -0,0 +1,47 @@
<script lang="ts" generics="TData extends Record<string, any>">
import { buttonVariants } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { m } from '$lib/paraglide/messages';
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
import Settings2Icon from '@lucide/svelte/icons/settings-2';
let {
columns,
selectedColumns = $bindable([])
}: { columns: AdvancedTableColumn<TData>[]; selectedColumns: string[] } = $props();
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger
class={buttonVariants({
variant: 'outline',
size: 'sm',
class: 'ml-auto h-8'
})}
>
<Settings2Icon />
<span class="hidden md:flex">{m.view()}</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>{m.toggle_columns()}</DropdownMenu.Label>
<DropdownMenu.Separator />
{#each columns as column (column)}
<DropdownMenu.CheckboxItem
closeOnSelect={false}
checked={selectedColumns.includes(column.column ?? column.key!)}
onCheckedChange={(v) => {
const key = column.column ?? column.key!;
if (v) {
selectedColumns = [...selectedColumns, key];
} else {
selectedColumns = selectedColumns.filter((c) => c !== key);
}
}}
>
{column.label}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -0,0 +1,128 @@
<script lang="ts" generics="TData, TValue">
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import * as Command from '$lib/components/ui/command/index.js';
import * as Popover from '$lib/components/ui/popover/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import { m } from '$lib/paraglide/messages';
import { cn } from '$lib/utils/style';
import CheckIcon from '@lucide/svelte/icons/check';
import ListFilterIcon from '@lucide/svelte/icons/list-filter';
import type { Component } from 'svelte';
let {
title,
options,
selectedValues = new Set<string | boolean>(),
showCheckboxes = true,
onChanged = (selected: Set<string | boolean>) => {}
}: {
title: string;
options: {
label: string;
value: string | boolean;
icon?: Component;
}[];
selectedValues?: Set<string | boolean>;
showCheckboxes?: boolean;
onChanged?: (selected: Set<string | boolean>) => void;
} = $props();
let open = $state(false);
</script>
<Popover.Root bind:open>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
size="sm"
class="h-8 border-dashed"
data-testid={`facet-${title.toLowerCase()}-trigger`}
>
<ListFilterIcon />
{title}
{#if selectedValues.size > 0}
<Separator orientation="vertical" class="mx-2 h-4" />
<Badge variant="secondary" class="rounded-sm px-1 font-normal lg:hidden">
{selectedValues.size}
</Badge>
<div class="hidden space-x-1 lg:flex">
{#if selectedValues.size > 2}
<Badge variant="secondary" class="rounded-sm px-1 font-normal">
Count: {selectedValues.size}
</Badge>
{:else}
{#each options.filter((opt) => selectedValues.has(opt.value)) as option (option)}
<Badge variant="secondary" class="rounded-sm px-1 font-normal">
{option.label}
</Badge>
{/each}
{/if}
</div>
{/if}
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content
class="w-[200px] p-0"
align="start"
data-testid={`facet-${title.toLowerCase()}-content`}
>
<Command.Root>
<Command.List>
<Command.Empty>{m.no_items_found()}</Command.Empty>
<Command.Group>
{#each options as option (option)}
{@const isSelected = selectedValues.has(option.value)}
<Command.Item
data-testid={`facet-${title.toLowerCase()}-option-${String(option.value)}`}
onSelect={() => {
if (isSelected) {
selectedValues = new Set([...selectedValues].filter((v) => v !== option.value));
} else {
selectedValues = new Set([...selectedValues, option.value]);
}
onChanged(selectedValues);
}}
>
{#if showCheckboxes}
<div
class={cn(
'border-primary mr-2 flex size-4 items-center justify-center rounded-sm border',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon class="size-4" />
</div>
{/if}
{#if option.icon}
{@const Icon = option.icon}
<Icon class="text-muted-foreground" />
{/if}
<span>{option.label}</span>
</Command.Item>
{/each}
</Command.Group>
{#if selectedValues.size > 0}
<Command.Separator />
<Command.Group>
<Command.Item
onSelect={() => {
selectedValues = new Set();
onChanged(selectedValues);
open = false;
}}
>
{m.clear_filters()}
</Command.Item>
</Command.Group>
{/if}
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>

View File

@@ -0,0 +1,67 @@
<script lang="ts" generics="TData extends Record<string, any>">
import { Input } from '$lib/components/ui/input/index.js';
import { m } from '$lib/paraglide/messages';
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
import type { ListRequestOptions } from '$lib/types/list-request.type';
import { debounced } from '$lib/utils/debounce-util';
import AdvancedTableColumnSelection from './advanced-table-column-selection.svelte';
import AdvancedTableFilter from './advanced-table-filter.svelte';
let {
columns,
visibleColumns = $bindable(),
requestOptions,
searchValue = $bindable(),
withoutSearch = false,
onFilterChange,
refresh
}: {
columns: AdvancedTableColumn<TData>[];
visibleColumns: string[];
requestOptions: ListRequestOptions;
searchValue?: string;
withoutSearch?: boolean;
onFilterChange?: (selected: Set<string | boolean>, column: string) => void;
refresh: () => Promise<void>;
} = $props();
let filterableColumns = $derived(
columns
.filter((c) => c.filterableValues)
.map((c) => ({
name: c.label!,
column: c.column!,
options: c.filterableValues!
}))
);
const onSearch = debounced(async (search: string) => {
requestOptions.search = search;
await refresh();
searchValue = search;
}, 300);
</script>
<div class="mb-4 flex flex-wrap items-end justify-between gap-2">
<div class="flex flex-1 items-center gap-2 has-[>:nth-child(3)]:flex-wrap">
{#if !withoutSearch}
<Input
value={searchValue}
class="relative z-50 w-full sm:max-w-xs"
placeholder={m.search()}
type="text"
oninput={(e: Event) => onSearch((e.currentTarget as HTMLInputElement).value)}
/>
{/if}
{#each filterableColumns as col}
<AdvancedTableFilter
title={col.name}
options={col.options}
selectedValues={new Set(requestOptions.filters?.[col.column] || [])}
onChanged={(selected) => onFilterChange?.(selected, col.column)}
/>
{/each}
<AdvancedTableColumnSelection {columns} bind:selectedColumns={visibleColumns} />
</div>
</div>

View File

@@ -0,0 +1,356 @@
<script lang="ts" generics="T extends {id:string}">
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import * as Pagination from '$lib/components/ui/pagination';
import * as Select from '$lib/components/ui/select';
import * as Table from '$lib/components/ui/table/index.js';
import Empty from '$lib/icons/empty.svelte';
import { m } from '$lib/paraglide/messages';
import type {
AdvancedTableColumn,
CreateAdvancedTableActions
} from '$lib/types/advanced-table.type';
import type { ListRequestOptions, Paginated, SortRequest } from '$lib/types/list-request.type';
import { cn } from '$lib/utils/style';
import { ChevronDown, LucideEllipsis } from '@lucide/svelte';
import { PersistedState } from 'runed';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import Button, { buttonVariants } from '../ui/button/button.svelte';
import * as DropdownMenu from '../ui/dropdown-menu/index.js';
import { Skeleton } from '../ui/skeleton';
import AdvancedTableToolbar from './advanced-table-toolbar.svelte';
let {
id,
selectedIds = $bindable(),
withoutSearch = false,
selectionDisabled = false,
fetchCallback,
defaultSort,
columns,
actions
}: {
id: string;
selectedIds?: string[];
withoutSearch?: boolean;
selectionDisabled?: boolean;
fetchCallback: (requestOptions: ListRequestOptions) => Promise<Paginated<T>>;
defaultSort?: SortRequest;
columns: AdvancedTableColumn<T>[];
actions?: CreateAdvancedTableActions<T>;
} = $props();
let items: Paginated<T> | undefined = $state();
let searchValue = $state('');
const availablePageSizes: number[] = [20, 50, 100];
type TablePreferences = {
visibleColumns: string[];
paginationLimit: number;
sort?: SortRequest;
filters?: Record<string, (string | boolean)[]>;
length?: number;
};
const tablePreferences = new PersistedState<TablePreferences>(`table-${id}-preferences`, {
visibleColumns: columns.filter((c) => !c.hidden).map((c) => c.column ?? c.key!),
paginationLimit: 20,
filters: initializeFilters()
});
const requestOptions = $state<ListRequestOptions>({
sort: tablePreferences.current.sort ?? defaultSort,
pagination: { limit: tablePreferences.current.paginationLimit, page: 1 },
filters: tablePreferences.current.filters
});
let visibleColumns = $derived(
columns.filter(
(c) => tablePreferences.current.visibleColumns?.includes(c.column ?? c.key!) ?? []
)
);
onMount(async () => {
const urlParams = new URLSearchParams(window.location.search);
const page = parseInt(urlParams.get(`${id}-page`) ?? '') || undefined;
if (page) {
requestOptions.pagination!.page = page;
}
await refresh();
});
let allChecked = $derived.by(() => {
if (!selectedIds || !items || items.data.length === 0) return false;
for (const item of items!.data) {
if (!selectedIds.includes(item.id)) {
return false;
}
}
return true;
});
async function onAllCheck(checked: boolean) {
const pageIds = items!.data.map((item) => item.id);
const current = selectedIds ?? [];
if (checked) {
selectedIds = Array.from(new Set([...current, ...pageIds]));
} else {
selectedIds = current.filter((id) => !pageIds.includes(id));
}
}
async function onCheck(checked: boolean, id: string) {
const current = selectedIds ?? [];
if (checked) {
selectedIds = Array.from(new Set([...current, id]));
} else {
selectedIds = current.filter((selectedId) => selectedId !== id);
}
}
async function onPageChange(page: number) {
changePageState(page);
await refresh();
}
async function onPageSizeChange(size: number) {
requestOptions.pagination = { limit: size, page: 1 };
tablePreferences.current.paginationLimit = size;
await refresh();
}
async function onFilterChange(selected: Set<string | boolean>, column: string) {
requestOptions.filters = {
...requestOptions.filters,
[column]: Array.from(selected)
};
tablePreferences.current.filters = requestOptions.filters;
await refresh();
}
async function onSort(column?: string) {
if (!column) return;
const isSameColumn = requestOptions.sort?.column === column;
const nextDirection: 'asc' | 'desc' =
isSameColumn && requestOptions.sort?.direction === 'asc' ? 'desc' : 'asc';
requestOptions.sort = { column, direction: nextDirection };
tablePreferences.current.sort = requestOptions.sort;
await refresh();
}
function changePageState(page: number) {
const url = new URL(window.location.href);
url.searchParams.set(`${id}-page`, page.toString());
history.replaceState(history.state, '', url.toString());
requestOptions.pagination!.page = page;
}
function updateListLength(totalItems: number) {
tablePreferences.current.length =
totalItems > tablePreferences.current.paginationLimit
? tablePreferences.current.paginationLimit
: totalItems;
}
function initializeFilters() {
const filters: Record<string, (string | boolean)[]> = {};
columns.forEach((c) => {
if (c.filterableValues) {
filters[c.column!] = [];
}
});
return filters;
}
export async function refresh() {
items = await fetchCallback(requestOptions);
changePageState(items.pagination.currentPage);
updateListLength(items.pagination.totalItems);
}
</script>
<AdvancedTableToolbar
{columns}
bind:visibleColumns={tablePreferences.current.visibleColumns}
{requestOptions}
{searchValue}
{withoutSearch}
{refresh}
{onFilterChange}
/>
{#if (items?.pagination.totalItems === 0 && searchValue === '') || tablePreferences.current.length === 0}
<div class="my-5 flex flex-col items-center">
<Empty class="text-muted-foreground h-20" />
<p class="text-muted-foreground mt-3 text-sm">{m.no_items_found()}</p>
</div>
{:else}
{#if !items}
<div>
{#each Array((tablePreferences.current.length || 10) + 1) as _}
<div>
<Skeleton class="mt-3 h-[45px] w-full rounded-lg" />
</div>
{/each}
</div>
{:else}
<div in:fade>
<Table.Root class="min-w-full table-auto overflow-x-auto">
<Table.Header>
<Table.Row>
{#if selectedIds}
<Table.Head class="w-12">
<Checkbox
disabled={selectionDisabled}
checked={allChecked}
onCheckedChange={(c: boolean) => onAllCheck(c as boolean)}
/>
</Table.Head>
{/if}
{#each visibleColumns as column}
<Table.Head class={cn(column.sortable && 'p-0')}>
{#if column.sortable}
<Button
variant="ghost"
class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent"
onclick={() => onSort(column.column)}
>
<span class="flex items-center">
{column.label}
<ChevronDown
class={cn(
'ml-2 size-4 transition-all',
requestOptions.sort?.column === column.column
? requestOptions.sort?.direction === 'asc'
? 'rotate-180 opacity-100'
: 'opacity-100'
: 'opacity-0'
)}
/>
</span>
</Button>
{:else}
{column.label}
{/if}
</Table.Head>
{/each}
{#if actions}
<Table.Head align="right" class="w-12">
<span class="sr-only">{m.actions()}</span>
</Table.Head>
{/if}
</Table.Row>
</Table.Header>
<Table.Body>
{#each items.data as item}
<Table.Row class={selectedIds?.includes(item.id) ? 'bg-muted/20' : ''}>
{#if selectedIds}
<Table.Cell class="w-12">
<Checkbox
disabled={selectionDisabled}
checked={selectedIds.includes(item.id)}
onCheckedChange={(c: boolean) => onCheck(c, item.id)}
/>
</Table.Cell>
{/if}
{#each visibleColumns as column}
<Table.Cell>
{#if column.value}
{column.value(item)}
{:else if column.cell}
{@render column.cell({ item })}
{:else if column.column && typeof item[column.column] === 'boolean'}
{item[column.column] ? m.enabled() : m.disabled()}
{:else if column.column}
{item[column.column]}
{/if}
</Table.Cell>
{/each}
{#if actions}
<Table.Cell align="right" class="w-12 py-0">
<DropdownMenu.Root>
<DropdownMenu.Trigger
class={buttonVariants({ variant: 'ghost', size: 'icon' })}
>
<LucideEllipsis class="size-4" />
<span class="sr-only">{m.toggle_menu()}</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each actions(item).filter((a) => !a.hidden) as action}
<DropdownMenu.Item
onclick={() => action.onClick(item)}
disabled={action.disabled}
class={action.variant === 'danger'
? 'text-red-500 focus:!text-red-700'
: ''}
>
{#if action.icon}
{@const Icon = action.icon}
<Icon class="mr-2 size-4" />
{/if}
{action.label}
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
{/if}
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{/if}
<div class="mt-5 flex flex-col-reverse items-center justify-between gap-3 sm:flex-row">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">{m.items_per_page()}</p>
<Select.Root
type="single"
value={items?.pagination.itemsPerPage.toString()}
onValueChange={(v) => onPageSizeChange(Number(v))}
>
<Select.Trigger class="h-9 w-[80px]">
{items?.pagination.itemsPerPage}
</Select.Trigger>
<Select.Content>
{#each availablePageSizes as size}
<Select.Item value={size.toString()}>{size}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Pagination.Root
class="mx-0 w-auto"
count={items?.pagination.totalItems || 0}
perPage={items?.pagination.itemsPerPage}
{onPageChange}
page={items?.pagination.currentPage}
>
{#snippet children({ pages })}
<Pagination.Content class="flex justify-end">
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type !== 'ellipsis' && page.value != 0}
<Pagination.Item>
<Pagination.Link {page} isActive={items?.pagination.currentPage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
</div>
{/if}

View File

@@ -0,0 +1,7 @@
import Root from "./skeleton.svelte";
export {
Root,
//
Root as Skeleton,
};

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils/style.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
</script>
<div
bind:this={ref}
data-slot="skeleton"
class={cn("bg-accent animate-pulse rounded-md", className)}
{...restProps}
></div>

View File

@@ -14,7 +14,7 @@
bind:this={ref} bind:this={ref}
data-slot="table-cell" data-slot="table-cell"
class={cn( class={cn(
'p-4 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', 'py-3 px-4 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className
)} )}
{...restProps} {...restProps}

View File

@@ -1,11 +1,9 @@
<script lang="ts"> <script lang="ts">
import AdvancedTable from '$lib/components/advanced-table.svelte'; import AdvancedTable from '$lib/components/table/advanced-table.svelte';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserGroupService from '$lib/services/user-group-service'; import UserGroupService from '$lib/services/user-group-service';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
import type { UserGroup } from '$lib/types/user-group.type'; import type { UserGroupWithUserCount } from '$lib/types/user-group.type';
import { onMount } from 'svelte';
let { let {
selectionDisabled = false, selectionDisabled = false,
@@ -17,30 +15,27 @@
const userGroupService = new UserGroupService(); const userGroupService = new UserGroupService();
let groups: Paginated<UserGroup> | undefined = $state(); const columns: AdvancedTableColumn<UserGroupWithUserCount>[] = [
let requestOptions: SearchPaginationSortRequest = $state({ { label: 'ID', column: 'id', hidden: true },
sort: { { label: m.friendly_name(), column: 'friendlyName', sortable: true },
column: 'friendlyName', { label: m.name(), column: 'name', sortable: true },
direction: 'asc' { label: m.user_count(), column: 'userCount', sortable: true },
} {
}); label: m.created(),
column: 'createdAt',
onMount(async () => { sortable: true,
groups = await userGroupService.list(requestOptions); hidden: true,
}); value: (item) => new Date(item.createdAt).toLocaleString()
},
{ label: m.ldap_id(), column: 'ldapId', hidden: true }
];
</script> </script>
{#if groups} <AdvancedTable
<AdvancedTable id="user-group-selection"
items={groups} fetchCallback={userGroupService.list}
{requestOptions} defaultSort={{ column: 'friendlyName', direction: 'asc' }}
onRefresh={async (o) => (groups = await userGroupService.list(o))} bind:selectedIds={selectedGroupIds}
columns={[{ label: m.name(), sortColumn: 'friendlyName' }]} {selectionDisabled}
bind:selectedIds={selectedGroupIds} {columns}
{selectionDisabled} />
>
{#snippet rows({ item })}
<Table.Cell>{item.friendlyName}</Table.Cell>
{/snippet}
</AdvancedTable>
{/if}

View File

@@ -1,21 +1,19 @@
import type { ApiKey, ApiKeyCreate, ApiKeyResponse } from '$lib/types/api-key.type'; import type { ApiKey, ApiKeyCreate, ApiKeyResponse } from '$lib/types/api-key.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import APIService from './api-service'; import APIService from './api-service';
export default class ApiKeyService extends APIService { export default class ApiKeyService extends APIService {
async list(options?: SearchPaginationSortRequest) { list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/api-keys', { const res = await this.api.get('/api-keys', { params: options });
params: options
});
return res.data as Paginated<ApiKey>; return res.data as Paginated<ApiKey>;
} };
async create(data: ApiKeyCreate): Promise<ApiKeyResponse> { create = async (data: ApiKeyCreate): Promise<ApiKeyResponse> => {
const res = await this.api.post('/api-keys', data); const res = await this.api.post('/api-keys', data);
return res.data as ApiKeyResponse; return res.data as ApiKeyResponse;
} };
async revoke(id: string): Promise<void> { revoke = async (id: string): Promise<void> => {
await this.api.delete(`/api-keys/${id}`); await this.api.delete(`/api-keys/${id}`);
} };
} }

View File

@@ -1,15 +1,13 @@
import axios from 'axios'; import axios from 'axios';
abstract class APIService { abstract class APIService {
api = axios.create({ protected api = axios.create({ baseURL: '/api' });
baseURL: '/api'
});
constructor() { constructor() {
if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) { if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) {
this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL; this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL;
} }
} }
} }
export default APIService; export default APIService;

View File

@@ -3,39 +3,33 @@ import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-
import APIService from './api-service'; import APIService from './api-service';
export default class AppConfigService extends APIService { export default class AppConfigService extends APIService {
async list(showAll = false) { list = async (showAll = false) => {
let url = '/application-configuration'; let url = '/application-configuration';
if (showAll) { if (showAll) url += '/all';
url += '/all';
}
const { data } = await this.api.get<AppConfigRawResponse>(url); const { data } = await this.api.get<AppConfigRawResponse>(url);
return this.parseConfigList(data); return parseConfigList(data);
} };
async update(appConfig: AllAppConfig) { update = async (appConfig: AllAppConfig) => {
// Convert all values to string, stringifying JSON where needed // Convert all values to string, stringifying JSON where needed
const appConfigConvertedToString: Record<string, string> = {}; const appConfigConvertedToString: Record<string, string> = {};
for (const key in appConfig) { for (const key in appConfig) {
const value = (appConfig as any)[key]; const value = (appConfig as any)[key];
if (typeof value === 'object' && value !== null) { appConfigConvertedToString[key] =
appConfigConvertedToString[key] = JSON.stringify(value); typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
} else {
appConfigConvertedToString[key] = String(value);
}
} }
const res = await this.api.put('/application-configuration', appConfigConvertedToString); const res = await this.api.put('/application-configuration', appConfigConvertedToString);
return this.parseConfigList(res.data); return parseConfigList(res.data);
} };
async updateFavicon(favicon: File) { updateFavicon = async (favicon: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', favicon!); formData.append('file', favicon!);
await this.api.put(`/application-images/favicon`, formData); await this.api.put(`/application-images/favicon`, formData);
} }
async updateLogo(logo: File, light = true) { updateLogo = async (logo: File, light = true) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', logo!); formData.append('file', logo!);
@@ -43,52 +37,52 @@ export default class AppConfigService extends APIService {
params: { light } params: { light }
}); });
cachedApplicationLogo.bustCache(light); cachedApplicationLogo.bustCache(light);
} };
async updateBackgroundImage(backgroundImage: File) { updateBackgroundImage = async (backgroundImage: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', backgroundImage!); formData.append('file', backgroundImage!);
await this.api.put(`/application-images/background`, formData); await this.api.put(`/application-images/background`, formData);
cachedBackgroundImage.bustCache(); cachedBackgroundImage.bustCache();
} };
async sendTestEmail() { sendTestEmail = async () => {
await this.api.post('/application-configuration/test-email'); await this.api.post('/application-configuration/test-email');
} };
async syncLdap() { syncLdap = async () => {
await this.api.post('/application-configuration/sync-ldap'); await this.api.post('/application-configuration/sync-ldap');
} };
}
private parseConfigList(data: AppConfigRawResponse) { function parseConfigList(data: AppConfigRawResponse) {
const appConfig: Partial<AllAppConfig> = {}; const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => { data.forEach(({ key, value }) => {
(appConfig as any)[key] = this.parseValue(value); (appConfig as any)[key] = parseValue(value);
}); });
return appConfig as AllAppConfig; return appConfig as AllAppConfig;
} }
private parseValue(value: string) { function parseValue(value: string) {
// Try to parse JSON first // Try to parse JSON first
try { try {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) { if (typeof parsed === 'object' && parsed !== null) {
return parsed; return parsed;
}
value = String(parsed);
} catch {}
// Handle rest of the types
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
return parseFloat(value);
} else {
return value;
} }
value = String(parsed);
} catch {}
// Handle rest of the types
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
return parseFloat(value);
} else {
return value;
} }
} }

View File

@@ -1,34 +1,25 @@
import type { AuditLog, AuditLogFilter } from '$lib/types/audit-log.type'; import type { AuditLog } from '$lib/types/audit-log.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import APIService from './api-service'; import APIService from './api-service';
class AuditLogService extends APIService { export default class AuditLogService extends APIService {
async list(options?: SearchPaginationSortRequest) { list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/audit-logs', { const res = await this.api.get('/audit-logs', { params: options });
params: options
});
return res.data as Paginated<AuditLog>; return res.data as Paginated<AuditLog>;
} };
async listAllLogs(options?: SearchPaginationSortRequest, filters?: AuditLogFilter) { listAllLogs = async (options?: ListRequestOptions) => {
const res = await this.api.get('/audit-logs/all', { const res = await this.api.get('/audit-logs/all', { params: options });
params: {
...options,
filters
}
});
return res.data as Paginated<AuditLog>; return res.data as Paginated<AuditLog>;
} };
async listClientNames() { listClientNames = async () => {
const res = await this.api.get<string[]>('/audit-logs/filters/client-names'); const res = await this.api.get<string[]>('/audit-logs/filters/client-names');
return res.data; return res.data;
} };
async listUsers() { listUsers = async () => {
const res = await this.api.get<Record<string, string>>('/audit-logs/filters/users'); const res = await this.api.get<Record<string, string>>('/audit-logs/filters/users');
return res.data; return res.data;
} };
} }
export default AuditLogService;

View File

@@ -2,18 +2,18 @@ import type { CustomClaim } from '$lib/types/custom-claim.type';
import APIService from './api-service'; import APIService from './api-service';
export default class CustomClaimService extends APIService { export default class CustomClaimService extends APIService {
async getSuggestions() { getSuggestions = async () => {
const res = await this.api.get('/custom-claims/suggestions'); const res = await this.api.get('/custom-claims/suggestions');
return res.data as string[]; return res.data as string[];
} };
async updateUserCustomClaims(userId: string, claims: CustomClaim[]) { updateUserCustomClaims = async (userId: string, claims: CustomClaim[]) => {
const res = await this.api.put(`/custom-claims/user/${userId}`, claims); const res = await this.api.put(`/custom-claims/user/${userId}`, claims);
return res.data as CustomClaim[]; return res.data as CustomClaim[];
} };
async updateUserGroupCustomClaims(userGroupId: string, claims: CustomClaim[]) { updateUserGroupCustomClaims = async (userGroupId: string, claims: CustomClaim[]) => {
const res = await this.api.put(`/custom-claims/user-group/${userGroupId}`, claims); const res = await this.api.put(`/custom-claims/user-group/${userGroupId}`, claims);
return res.data as CustomClaim[]; return res.data as CustomClaim[];
} };
} }

View File

@@ -1,3 +1,4 @@
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type { import type {
AccessibleOidcClient, AccessibleOidcClient,
AuthorizeResponse, AuthorizeResponse,
@@ -9,12 +10,11 @@ import type {
OidcClientWithAllowedUserGroupsCount, OidcClientWithAllowedUserGroupsCount,
OidcDeviceCodeInfo OidcDeviceCodeInfo
} from '$lib/types/oidc.type'; } from '$lib/types/oidc.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util'; import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import APIService from './api-service'; import APIService from './api-service';
class OidcService extends APIService { class OidcService extends APIService {
async authorize( authorize = async (
clientId: string, clientId: string,
scope: string, scope: string,
callbackURL: string, callbackURL: string,
@@ -22,7 +22,7 @@ class OidcService extends APIService {
codeChallenge?: string, codeChallenge?: string,
codeChallengeMethod?: string, codeChallengeMethod?: string,
reauthenticationToken?: string reauthenticationToken?: string
) { ) => {
const res = await this.api.post('/oidc/authorize', { const res = await this.api.post('/oidc/authorize', {
scope, scope,
nonce, nonce,
@@ -34,101 +34,99 @@ class OidcService extends APIService {
}); });
return res.data as AuthorizeResponse; return res.data as AuthorizeResponse;
} };
async isAuthorizationRequired(clientId: string, scope: string) { isAuthorizationRequired = async (clientId: string, scope: string) => {
const res = await this.api.post('/oidc/authorization-required', { const res = await this.api.post('/oidc/authorization-required', {
scope, scope,
clientId clientId
}); });
return res.data.authorizationRequired as boolean; return res.data.authorizationRequired as boolean;
} };
async listClients(options?: SearchPaginationSortRequest) { listClients = async (options?: ListRequestOptions) => {
const res = await this.api.get('/oidc/clients', { const res = await this.api.get('/oidc/clients', {
params: options params: options
}); });
return res.data as Paginated<OidcClientWithAllowedUserGroupsCount>; return res.data as Paginated<OidcClientWithAllowedUserGroupsCount>;
} };
async createClient(client: OidcClientCreate) { createClient = async (client: OidcClientCreate) =>
return (await this.api.post('/oidc/clients', client)).data as OidcClient; (await this.api.post('/oidc/clients', client)).data as OidcClient;
}
async removeClient(id: string) { removeClient = async (id: string) => {
await this.api.delete(`/oidc/clients/${id}`); await this.api.delete(`/oidc/clients/${id}`);
} };
async getClient(id: string) { getClient = async (id: string) =>
return (await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups; (await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups;
}
async getClientMetaData(id: string) { getClientMetaData = async (id: string) =>
return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData; (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
}
async updateClient(id: string, client: OidcClientUpdate) { updateClient = async (id: string, client: OidcClientUpdate) =>
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient; (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
}
async updateClientLogo(client: OidcClient, image: File | null) { updateClientLogo = async (client: OidcClient, image: File | null, light: boolean = true) => {
if (client.hasLogo && !image) { const hasLogo = light ? client.hasLogo : client.hasDarkLogo;
await this.removeClientLogo(client.id);
if (hasLogo && !image) {
await this.removeClientLogo(client.id, light);
return; return;
} }
if (!client.hasLogo && !image) { if (!hasLogo && !image) {
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('file', image!); formData.append('file', image!);
await this.api.post(`/oidc/clients/${client.id}/logo`, formData); await this.api.post(`/oidc/clients/${client.id}/logo`, formData, {
cachedOidcClientLogo.bustCache(client.id); params: { light }
} });
cachedOidcClientLogo.bustCache(client.id, light);
};
async removeClientLogo(id: string) { removeClientLogo = async (id: string, light: boolean = true) => {
await this.api.delete(`/oidc/clients/${id}/logo`); await this.api.delete(`/oidc/clients/${id}/logo`, {
cachedOidcClientLogo.bustCache(id); params: { light }
} });
cachedOidcClientLogo.bustCache(id, light);
};
async createClientSecret(id: string) { createClientSecret = async (id: string) =>
return (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string; (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
}
async updateAllowedUserGroups(id: string, userGroupIds: string[]) { updateAllowedUserGroups = async (id: string, userGroupIds: string[]) => {
const res = await this.api.put(`/oidc/clients/${id}/allowed-user-groups`, { userGroupIds }); const res = await this.api.put(`/oidc/clients/${id}/allowed-user-groups`, { userGroupIds });
return res.data as OidcClientWithAllowedUserGroups; return res.data as OidcClientWithAllowedUserGroups;
} };
async verifyDeviceCode(userCode: string) { verifyDeviceCode = async (userCode: string) => {
return await this.api.post(`/oidc/device/verify?code=${userCode}`); return await this.api.post(`/oidc/device/verify?code=${userCode}`);
} };
async getDeviceCodeInfo(userCode: string): Promise<OidcDeviceCodeInfo> { getDeviceCodeInfo = async (userCode: string): Promise<OidcDeviceCodeInfo> => {
const response = await this.api.get(`/oidc/device/info?code=${userCode}`); const response = await this.api.get(`/oidc/device/info?code=${userCode}`);
return response.data; return response.data;
} };
async getClientPreview(id: string, userId: string, scopes: string) { getClientPreview = async (id: string, userId: string, scopes: string) => {
const response = await this.api.get(`/oidc/clients/${id}/preview/${userId}`, { const response = await this.api.get(`/oidc/clients/${id}/preview/${userId}`, {
params: { scopes } params: { scopes }
}); });
return response.data; return response.data;
} };
async listOwnAccessibleClients(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/oidc/users/me/clients', {
params: options
});
listOwnAccessibleClients = async (options?: ListRequestOptions) => {
const res = await this.api.get('/oidc/users/me/clients', { params: options });
return res.data as Paginated<AccessibleOidcClient>; return res.data as Paginated<AccessibleOidcClient>;
} };
async revokeOwnAuthorizedClient(clientId: string) { revokeOwnAuthorizedClient = async (clientId: string) => {
await this.api.delete(`/oidc/users/me/authorized-clients/${clientId}`); await this.api.delete(`/oidc/users/me/authorized-clients/${clientId}`);
} };
} }
export default OidcService; export default OidcService;

View File

@@ -1,4 +1,4 @@
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type { import type {
UserGroupCreate, UserGroupCreate,
UserGroupWithUserCount, UserGroupWithUserCount,
@@ -7,34 +7,32 @@ import type {
import APIService from './api-service'; import APIService from './api-service';
export default class UserGroupService extends APIService { export default class UserGroupService extends APIService {
async list(options?: SearchPaginationSortRequest) { list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/user-groups', { const res = await this.api.get('/user-groups', { params: options });
params: options
});
return res.data as Paginated<UserGroupWithUserCount>; return res.data as Paginated<UserGroupWithUserCount>;
} };
async get(id: string) { get = async (id: string) => {
const res = await this.api.get(`/user-groups/${id}`); const res = await this.api.get(`/user-groups/${id}`);
return res.data as UserGroupWithUsers; return res.data as UserGroupWithUsers;
} };
async create(user: UserGroupCreate) { create = async (user: UserGroupCreate) => {
const res = await this.api.post('/user-groups', user); const res = await this.api.post('/user-groups', user);
return res.data as UserGroupWithUsers; return res.data as UserGroupWithUsers;
} };
async update(id: string, user: UserGroupCreate) { update = async (id: string, user: UserGroupCreate) => {
const res = await this.api.put(`/user-groups/${id}`, user); const res = await this.api.put(`/user-groups/${id}`, user);
return res.data as UserGroupWithUsers; return res.data as UserGroupWithUsers;
} };
async remove(id: string) { remove = async (id: string) => {
await this.api.delete(`/user-groups/${id}`); await this.api.delete(`/user-groups/${id}`);
} };
async updateUsers(id: string, userIds: string[]) { updateUsers = async (id: string, userIds: string[]) => {
const res = await this.api.put(`/user-groups/${id}/users`, { userIds }); const res = await this.api.put(`/user-groups/${id}/users`, { userIds });
return res.data as UserGroupWithUsers; return res.data as UserGroupWithUsers;
} };
} }

View File

@@ -1,5 +1,5 @@
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type { SignupTokenDto } from '$lib/types/signup-token.type'; import type { SignupTokenDto } from '$lib/types/signup-token.type';
import type { UserGroup } from '$lib/types/user-group.type'; import type { UserGroup } from '$lib/types/user-group.type';
import type { User, UserCreate, UserSignUp } from '$lib/types/user.type'; import type { User, UserCreate, UserSignUp } from '$lib/types/user.type';
@@ -8,125 +8,113 @@ import { get } from 'svelte/store';
import APIService from './api-service'; import APIService from './api-service';
export default class UserService extends APIService { export default class UserService extends APIService {
async list(options?: SearchPaginationSortRequest) { list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/users', { const res = await this.api.get('/users', { params: options });
params: options
});
return res.data as Paginated<User>; return res.data as Paginated<User>;
} };
async get(id: string) { get = async (id: string) => {
const res = await this.api.get(`/users/${id}`); const res = await this.api.get(`/users/${id}`);
return res.data as User; return res.data as User;
} };
async getCurrent() { getCurrent = async () => {
const res = await this.api.get('/users/me'); const res = await this.api.get('/users/me');
return res.data as User; return res.data as User;
} };
async create(user: UserCreate) { create = async (user: UserCreate) => {
const res = await this.api.post('/users', user); const res = await this.api.post('/users', user);
return res.data as User; return res.data as User;
} };
async getUserGroups(userId: string) { getUserGroups = async (userId: string) => {
const res = await this.api.get(`/users/${userId}/groups`); const res = await this.api.get(`/users/${userId}/groups`);
return res.data as UserGroup[]; return res.data as UserGroup[];
} };
async update(id: string, user: UserCreate) { update = async (id: string, user: UserCreate) => {
const res = await this.api.put(`/users/${id}`, user); const res = await this.api.put(`/users/${id}`, user);
return res.data as User; return res.data as User;
} };
async updateCurrent(user: UserCreate) { updateCurrent = async (user: UserCreate) => {
const res = await this.api.put('/users/me', user); const res = await this.api.put('/users/me', user);
return res.data as User; return res.data as User;
} };
async remove(id: string) { remove = async (id: string) => {
await this.api.delete(`/users/${id}`); await this.api.delete(`/users/${id}`);
} };
async updateProfilePicture(userId: string, image: File) { updateProfilePicture = async (userId: string, image: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', image!); formData.append('file', image!);
await this.api.put(`/users/${userId}/profile-picture`, formData); await this.api.put(`/users/${userId}/profile-picture`, formData);
cachedProfilePicture.bustCache(userId); cachedProfilePicture.bustCache(userId);
} };
async updateCurrentUsersProfilePicture(image: File) { updateCurrentUsersProfilePicture = async (image: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', image!); formData.append('file', image!);
await this.api.put('/users/me/profile-picture', formData); await this.api.put('/users/me/profile-picture', formData);
cachedProfilePicture.bustCache(get(userStore)!.id); cachedProfilePicture.bustCache(get(userStore)!.id);
} };
async resetCurrentUserProfilePicture() { resetCurrentUserProfilePicture = async () => {
await this.api.delete(`/users/me/profile-picture`); await this.api.delete(`/users/me/profile-picture`);
cachedProfilePicture.bustCache(get(userStore)!.id); cachedProfilePicture.bustCache(get(userStore)!.id);
} };
async resetProfilePicture(userId: string) { resetProfilePicture = async (userId: string) => {
await this.api.delete(`/users/${userId}/profile-picture`); await this.api.delete(`/users/${userId}/profile-picture`);
cachedProfilePicture.bustCache(userId); cachedProfilePicture.bustCache(userId);
} };
async createOneTimeAccessToken(userId: string = 'me', ttl?: string|number) { createOneTimeAccessToken = async (userId: string = 'me', ttl?: string | number) => {
const res = await this.api.post(`/users/${userId}/one-time-access-token`, { const res = await this.api.post(`/users/${userId}/one-time-access-token`, { userId, ttl });
userId,
ttl,
});
return res.data.token; return res.data.token;
} };
async createSignupToken(ttl: string|number, usageLimit: number) { createSignupToken = async (ttl: string | number, usageLimit: number) => {
const res = await this.api.post(`/signup-tokens`, { const res = await this.api.post(`/signup-tokens`, { ttl, usageLimit });
ttl,
usageLimit
});
return res.data.token; return res.data.token;
} };
async exchangeOneTimeAccessToken(token: string) { exchangeOneTimeAccessToken = async (token: string) => {
const res = await this.api.post(`/one-time-access-token/${token}`); const res = await this.api.post(`/one-time-access-token/${token}`);
return res.data as User; return res.data as User;
} };
async requestOneTimeAccessEmailAsUnauthenticatedUser(email: string, redirectPath?: string) { requestOneTimeAccessEmailAsUnauthenticatedUser = async (email: string, redirectPath?: string) => {
await this.api.post('/one-time-access-email', { email, redirectPath }); await this.api.post('/one-time-access-email', { email, redirectPath });
} };
async requestOneTimeAccessEmailAsAdmin(userId: string, ttl: string|number) { requestOneTimeAccessEmailAsAdmin = async (userId: string, ttl: string | number) => {
await this.api.post(`/users/${userId}/one-time-access-email`, { ttl }); await this.api.post(`/users/${userId}/one-time-access-email`, { ttl });
} };
async updateUserGroups(id: string, userGroupIds: string[]) { updateUserGroups = async (id: string, userGroupIds: string[]) => {
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds }); const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
return res.data as User; return res.data as User;
} };
async signup(data: UserSignUp) { signup = async (data: UserSignUp) => {
const res = await this.api.post(`/signup`, data); const res = await this.api.post(`/signup`, data);
return res.data as User; return res.data as User;
} };
async signupInitialUser(data: UserSignUp) { signupInitialUser = async (data: UserSignUp) => {
const res = await this.api.post(`/signup/setup`, data); const res = await this.api.post(`/signup/setup`, data);
return res.data as User; return res.data as User;
} };
async listSignupTokens(options?: SearchPaginationSortRequest) { listSignupTokens = async (options?: ListRequestOptions) => {
const res = await this.api.get('/signup-tokens', { const res = await this.api.get('/signup-tokens', { params: options });
params: options
});
return res.data as Paginated<SignupTokenDto>; return res.data as Paginated<SignupTokenDto>;
} };
async deleteSignupToken(tokenId: string) { deleteSignupToken = async (tokenId: string) => {
await this.api.delete(`/signup-tokens/${tokenId}`); await this.api.delete(`/signup-tokens/${tokenId}`);
} };
} }

View File

@@ -1,21 +1,13 @@
import { version as currentVersion } from '$app/environment'; import { version as currentVersion } from '$app/environment';
import axios from 'axios'; import APIService from './api-service';
async function getNewestVersion() { export default class VersionService extends APIService {
const response = await axios getNewestVersion = async () => {
.get('/api/version/latest', { const response = await this.api
timeout: 2000 .get('/version/latest', { timeout: 2000 })
}) .then((res) => res.data);
.then((res) => res.data); return response.latestVersion;
};
return response.latestVersion; getCurrentVersion = () => currentVersion;
} }
function getCurrentVersion() {
return currentVersion;
}
export default {
getNewestVersion,
getCurrentVersion,
};

View File

@@ -3,45 +3,36 @@ import type { User } from '$lib/types/user.type';
import APIService from './api-service'; import APIService from './api-service';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/browser'; import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/browser';
class WebAuthnService extends APIService { class WebAuthnService extends APIService {
async getRegistrationOptions() { getRegistrationOptions = async () => (await this.api.get(`/webauthn/register/start`)).data;
return (await this.api.get(`/webauthn/register/start`)).data;
}
async finishRegistration(body: RegistrationResponseJSON) { finishRegistration = async (body: RegistrationResponseJSON) =>
return (await this.api.post(`/webauthn/register/finish`, body)).data as Passkey; (await this.api.post(`/webauthn/register/finish`, body)).data as Passkey;
}
async getLoginOptions() { getLoginOptions = async () => (await this.api.get(`/webauthn/login/start`)).data;
return (await this.api.get(`/webauthn/login/start`)).data;
}
async finishLogin(body: AuthenticationResponseJSON) { finishLogin = async (body: AuthenticationResponseJSON) =>
return (await this.api.post(`/webauthn/login/finish`, body)).data as User; (await this.api.post(`/webauthn/login/finish`, body)).data as User;
}
async logout() { logout = async () => {
await this.api.post(`/webauthn/logout`); await this.api.post(`/webauthn/logout`);
userStore.clearUser(); userStore.clearUser();
} };
async listCredentials() { listCredentials = async () => (await this.api.get(`/webauthn/credentials`)).data as Passkey[];
return (await this.api.get(`/webauthn/credentials`)).data as Passkey[];
}
async removeCredential(id: string) { removeCredential = async (id: string) => {
await this.api.delete(`/webauthn/credentials/${id}`); await this.api.delete(`/webauthn/credentials/${id}`);
} };
async updateCredentialName(id: string, name: string) { updateCredentialName = async (id: string, name: string) => {
await this.api.patch(`/webauthn/credentials/${id}`, { name }); await this.api.patch(`/webauthn/credentials/${id}`, { name });
} };
async reauthenticate(body?: AuthenticationResponseJSON) { reauthenticate = async (body?: AuthenticationResponseJSON) => {
const res = await this.api.post('/webauthn/reauthenticate', body); const res = await this.api.post('/webauthn/reauthenticate', body);
return res.data.reauthenticationToken as string; return res.data.reauthenticationToken as string;
} };
} }
export default WebAuthnService; export default WebAuthnService;

View File

@@ -0,0 +1,26 @@
import type { Component, Snippet } from 'svelte';
export type AdvancedTableColumn<T extends Record<string, any>> = {
label: string;
column?: keyof T & string;
key?: string;
value?: (item: T) => string | number | boolean | undefined;
cell?: Snippet<[{ item: T }]>;
sortable?: boolean;
filterableValues?: {
label: string;
value: string | boolean;
icon?: Component;
}[];
hidden?: boolean;
};
export type CreateAdvancedTableActions<T extends Record<string, any>> = (item: T) => AdvancedTableAction<T>[];
export type AdvancedTableAction<T> = {
label: string;
icon?: Component;
variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost';
onClick: (item: T) => void;
hidden?: boolean;
disabled?: boolean;
};

View File

@@ -8,13 +8,11 @@ export type SortRequest = {
direction: 'asc' | 'desc'; direction: 'asc' | 'desc';
}; };
export type FilterMap = Record<string, string>; export type ListRequestOptions = {
export type SearchPaginationSortRequest = {
search?: string; search?: string;
pagination?: PaginationRequest; pagination?: PaginationRequest;
sort?: SortRequest; sort?: SortRequest;
filters?: FilterMap; filters?: Record<string, (string | boolean)[]>;
}; };
export type PaginationResponse = { export type PaginationResponse = {

View File

@@ -4,6 +4,7 @@ export type OidcClientMetaData = {
id: string; id: string;
name: string; name: string;
hasLogo: boolean; hasLogo: boolean;
hasDarkLogo: boolean;
requiresReauthentication: boolean; requiresReauthentication: boolean;
launchURL?: string; launchURL?: string;
}; };
@@ -37,17 +38,20 @@ export type OidcClientWithAllowedUserGroupsCount = OidcClient & {
allowedUserGroupsCount: number; allowedUserGroupsCount: number;
}; };
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>; export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo' | 'hasDarkLogo'>;
export type OidcClientCreate = OidcClientUpdate & { export type OidcClientCreate = OidcClientUpdate & {
id?: string; id?: string;
}; };
export type OidcClientUpdateWithLogo = OidcClientUpdate & { export type OidcClientUpdateWithLogo = OidcClientUpdate & {
logo: File | null | undefined; logo: File | null | undefined;
darkLogo: File | null | undefined;
}; };
export type OidcClientCreateWithLogo = OidcClientCreate & { export type OidcClientCreateWithLogo = OidcClientCreate & {
logo?: File | null; logo?: File | null;
logoUrl?: string; logoUrl?: string;
darkLogo?: File | null;
darkLogoUrl?: string;
}; };
export type OidcDeviceCodeInfo = { export type OidcDeviceCodeInfo = {

View File

@@ -9,73 +9,85 @@ type CachableImage = {
export const cachedApplicationLogo: CachableImage = { export const cachedApplicationLogo: CachableImage = {
getUrl: (light = true) => { getUrl: (light = true) => {
let url = '/api/application-images/logo'; const url = new URL('/api/application-images/logo', window.location.origin);
if (!light) { if (!light) url.searchParams.set('light', 'false');
url += '?light=false';
}
return getCachedImageUrl(url); return getCachedImageUrl(url);
}, },
bustCache: (light = true) => { bustCache: (light = true) => {
let url = '/api/application-images/logo'; const url = new URL('/api/application-images/logo', window.location.origin);
if (!light) { if (!light) url.searchParams.set('light', 'false');
url += '?light=false';
}
bustImageCache(url); bustImageCache(url);
} }
}; };
export const cachedBackgroundImage: CachableImage = { export const cachedBackgroundImage: CachableImage = {
getUrl: () => getCachedImageUrl('/api/application-images/background'), getUrl: () =>
bustCache: () => bustImageCache('/api/application-images/background') getCachedImageUrl(new URL('/api/application-images/background', window.location.origin)),
bustCache: () =>
bustImageCache(new URL('/api/application-images/background', window.location.origin))
}; };
export const cachedProfilePicture: CachableImage = { export const cachedProfilePicture: CachableImage = {
getUrl: (userId: string) => { getUrl: (userId: string) => {
const url = `/api/users/${userId}/profile-picture.png`; const url = new URL(`/api/users/${userId}/profile-picture.png`, window.location.origin);
return getCachedImageUrl(url); return getCachedImageUrl(url);
}, },
bustCache: (userId: string) => { bustCache: (userId: string) => {
const url = `/api/users/${userId}/profile-picture.png`; const url = new URL(`/api/users/${userId}/profile-picture.png`, window.location.origin);
bustImageCache(url); bustImageCache(url);
} }
}; };
export const cachedOidcClientLogo: CachableImage = { export const cachedOidcClientLogo: CachableImage = {
getUrl: (clientId: string) => { getUrl: (clientId: string, light = true) => {
const url = `/api/oidc/clients/${clientId}/logo`; const url = new URL(`/api/oidc/clients/${clientId}/logo`, window.location.origin);
if (!light) url.searchParams.set('light', 'false');
return getCachedImageUrl(url); return getCachedImageUrl(url);
}, },
bustCache: (clientId: string) => { bustCache: (clientId: string, light = true) => {
const url = `/api/oidc/clients/${clientId}/logo`; const url = new URL(`/api/oidc/clients/${clientId}/logo`, window.location.origin);
if (!light) url.searchParams.set('light', 'false');
bustImageCache(url); bustImageCache(url);
} }
}; };
function getCachedImageUrl(url: string) { function getCachedImageUrl(url: URL) {
const skipCacheUntil = getSkipCacheUntil(url); const baseKey = normalizeUrlForKey(url);
const skipCacheUntil = getSkipCacheUntil(baseKey);
const skipCache = skipCacheUntil > Date.now(); const skipCache = skipCacheUntil > Date.now();
const finalUrl = new URL(url.toString());
if (skipCache) { if (skipCache) {
const skipCacheParam = new URLSearchParams(); finalUrl.searchParams.set('skip-cache', skipCacheUntil.toString());
skipCacheParam.append('skip-cache', skipCacheUntil.toString());
url += '?' + skipCacheParam.toString();
} }
return url.toString(); return finalUrl.pathname + (finalUrl.search ? `?${finalUrl.searchParams.toString()}` : '');
} }
function bustImageCache(url: string) { function bustImageCache(url: URL) {
const skipCacheUntil: SkipCacheUntil = JSON.parse( const key = normalizeUrlForKey(url);
localStorage.getItem('skip-cache-until') ?? '{}' const expiresAt = Date.now() + 1000 * 60 * 15;
);
skipCacheUntil[hashKey(url)] = Date.now() + 1000 * 60 * 15; // 15 minutes const store: SkipCacheUntil = JSON.parse(localStorage.getItem('skip-cache-until') ?? '{}');
localStorage.setItem('skip-cache-until', JSON.stringify(skipCacheUntil)); store[key] = expiresAt;
localStorage.setItem('skip-cache-until', JSON.stringify(store));
} }
function getSkipCacheUntil(url: string) { function getSkipCacheUntil(key: string): number {
const skipCacheUntil: SkipCacheUntil = JSON.parse( const store: SkipCacheUntil = JSON.parse(localStorage.getItem('skip-cache-until') ?? '{}');
localStorage.getItem('skip-cache-until') ?? '{}' return store[key] ?? 0;
}
// Removes transient params and normalizes query order before hashing
function normalizeUrlForKey(url: URL) {
const u = new URL(url.toString());
u.searchParams.delete('skip-cache');
const sortedParams = new URLSearchParams(
[...u.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b))
); );
return skipCacheUntil[hashKey(url)] ?? 0; const normalized = u.pathname + (sortedParams.toString() ? `?${sortedParams.toString()}` : '');
return hashKey(normalized);
} }
function hashKey(key: string): string { function hashKey(key: string): string {
@@ -83,7 +95,7 @@ function hashKey(key: string): string {
for (let i = 0; i < key.length; i++) { for (let i = 0; i < key.length; i++) {
const char = key.charCodeAt(i); const char = key.charCodeAt(i);
hash = (hash << 5) - hash + char; hash = (hash << 5) - hash + char;
hash = hash & hash; hash |= 0;
} }
return Math.abs(hash).toString(36); return Math.abs(hash).toString(36);
} }

View File

@@ -13,6 +13,7 @@ export function getAuthRedirectPath(path: string, user: User | null) {
path.startsWith('/lc/') || path.startsWith('/lc/') ||
path == '/signup' || path == '/signup' ||
path == '/signup/setup' || path == '/signup/setup' ||
path == '/setup' ||
path.startsWith('/st/'); path.startsWith('/st/');
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path); const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/'); const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');

Some files were not shown because too many files have changed in this diff Show More