Compare commits

...

28 Commits

Author SHA1 Message Date
Elias Schneider
9c54e2e6b0 release: 1.8.0 2025-08-23 18:57:19 +02:00
Elias Schneider
a5efb95065 feat: allow custom client IDs (#864) 2025-08-23 18:41:05 +02:00
Elias Schneider
625f235740 fix: enable foreign key check for sqlite (#863)
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
2025-08-23 17:54:51 +02:00
Elias Schneider
2c122d413d refactor: run formatter 2025-08-23 17:46:59 +02:00
Elias Schneider
fc0c99a232 fix: oidc client advanced options color 2025-08-23 17:40:58 +02:00
Elias Schneider
24e274200f fix: ferated identities can't be cleared 2025-08-23 17:40:06 +02:00
Elias Schneider
0aab3f3c7a fix: authorization can't be revoked 2025-08-23 17:28:27 +02:00
Zeedif
182d809028 feat(signup): add default user groups and claims for new users (#812)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-08-22 14:25:02 +02:00
Elias Schneider
c51265dafb chore(translations): change alternative sign in methods text 2025-08-22 13:06:38 +02:00
Robert Mang
0cb039d35d feat: add option to OIDC client to require re-authentication (#747)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-08-22 08:56:40 +02:00
Alessandro (Ale) Segala
7ab0fd3028 fix: for one-time access tokens and signup tokens, pass TTLs instead of absolute expiration date (#855) 2025-08-22 08:02:56 +02:00
Maxime R
49f0fa423c chore: strip debug symbol from backend binary (#856) 2025-08-21 15:46:45 +00:00
Elias Schneider
61e63e411d chore(translations): update translations via Crowdin (#850) 2025-08-20 17:07:08 -05:00
Alessandro (Ale) Segala
9339e88a5a fix: move audit log call before TX is committed (#854) 2025-08-20 17:01:53 -05:00
Elias Schneider
fe003b927c fix: delete webauthn session after login to prevent replay attacks 2025-08-20 15:49:19 +02:00
Kyle Mendell
f5b5b1bd85 tests: use proper async calls for cleanupBackend function (#846) 2025-08-20 10:38:03 +02:00
James18232
d28bfac81f feat: login code font change (#851)
Co-authored-by: James18232 <80368042+James18232@users.noreply.github.com>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-08-19 14:10:57 +00:00
Elias Schneider
b04e3e8ecf chore(translations): update translations via Crowdin (#848) 2025-08-19 12:03:51 +02:00
Kyle Mendell
d77d8eb068 chore(translations): add Korean files 2025-08-18 14:53:19 -05:00
Elias Schneider
7cd88aca25 chore(translations): update translations via Crowdin (#841) 2025-08-18 11:21:27 -05:00
Gergő Gutyina
b5e6371eaa fix(deps): bump rollup from 4.45.3 to 4.46.3 (#845) 2025-08-18 07:44:42 -05:00
github-actions[bot]
544b98c1d0 chore: update AAGUIDs (#844)
Co-authored-by: stonith404 <58886915+stonith404@users.noreply.github.com>
2025-08-17 22:52:58 -05:00
Elias Schneider
3188e92257 feat: display all accessible oidc clients in the dashboard (#832)
Co-authored-by: Kyle Mendell <ksm@ofkm.us>
2025-08-17 22:47:34 +02:00
Elias Schneider
3fa2f9a162 chore(translations): update translations via Crowdin (#821) 2025-08-16 22:50:21 -05:00
James18232
7b1f6b8857 fix: ignore client secret if client is public (#836)
Co-authored-by: James18232 <80368042+James18232@users.noreply.github.com>
2025-08-16 17:55:32 +02:00
Alessandro (Ale) Segala
17d8893bdb chore: update deps and Go 1.25 (#833) 2025-08-14 22:33:27 -05:00
Elias Schneider
0e44f245af fix: non admin users can't revoke oidc client but see edit link 2025-08-12 09:46:15 +02:00
github-actions[bot]
824e8f1a0f chore: update AAGUIDs (#826)
Co-authored-by: stonith404 <58886915+stonith404@users.noreply.github.com>
2025-08-10 21:33:29 -05:00
113 changed files with 3191 additions and 1029 deletions

View File

@@ -32,9 +32,9 @@ jobs:
go-version-file: backend/go.mod go-version-file: backend/go.mod
- name: Run Golangci-lint - name: Run Golangci-lint
uses: golangci/golangci-lint-action@dec74fa03096ff515422f71d18d41307cacde373 # v7.0.0 uses: golangci/golangci-lint-action@v8.0.0
with: with:
version: v2.0.2 version: v2.4.0
args: --build-tags=exclude_frontend args: --build-tags=exclude_frontend
working-directory: backend working-directory: backend
only-new-issues: ${{ github.event_name == 'pull_request' }} only-new-issues: ${{ github.event_name == 'pull_request' }}

View File

@@ -1 +1 @@
1.7.0 1.8.0

View File

@@ -1,3 +1,28 @@
## [](https://github.com/pocket-id/pocket-id/compare/v1.7.0...v) (2025-08-23)
### Features
* add option to OIDC client to require re-authentication ([#747](https://github.com/pocket-id/pocket-id/issues/747)) ([0cb039d](https://github.com/pocket-id/pocket-id/commit/0cb039d35d49206011064e622f3bfd3d8f88720f))
* allow custom client IDs ([#864](https://github.com/pocket-id/pocket-id/issues/864)) ([a5efb95](https://github.com/pocket-id/pocket-id/commit/a5efb9506582884c70b9b1fd737ebdd44b101b47))
* display all accessible oidc clients in the dashboard ([#832](https://github.com/pocket-id/pocket-id/issues/832)) ([3188e92](https://github.com/pocket-id/pocket-id/commit/3188e92257afcaf7a16dd418e4c40626d7e1d034))
* login code font change ([#851](https://github.com/pocket-id/pocket-id/issues/851)) ([d28bfac](https://github.com/pocket-id/pocket-id/commit/d28bfac81fc24ee79e4896538a616f0a89ab30a5))
* **signup:** add default user groups and claims for new users ([#812](https://github.com/pocket-id/pocket-id/issues/812)) ([182d809](https://github.com/pocket-id/pocket-id/commit/182d8090286f9953171c6c410283be679889aca7))
### Bug Fixes
* authorization can't be revoked ([0aab3f3](https://github.com/pocket-id/pocket-id/commit/0aab3f3c7ad8c1b14939de3ded60c9f201eab8fc))
* delete webauthn session after login to prevent replay attacks ([fe003b9](https://github.com/pocket-id/pocket-id/commit/fe003b927ce7772692439992860c804de89ce424))
* **deps:** bump rollup from 4.45.3 to 4.46.3 ([#845](https://github.com/pocket-id/pocket-id/issues/845)) ([b5e6371](https://github.com/pocket-id/pocket-id/commit/b5e6371eaaf3d9e85d8b05c457487c4425fa8381))
* enable foreign key check for sqlite ([#863](https://github.com/pocket-id/pocket-id/issues/863)) ([625f235](https://github.com/pocket-id/pocket-id/commit/625f23574001ebd7074b8d98d448a2811847be16))
* ferated identities can't be cleared ([24e2742](https://github.com/pocket-id/pocket-id/commit/24e274200fe4002d01c58cc3fa74094b598d7599))
* for one-time access tokens and signup tokens, pass TTLs instead of absolute expiration date ([#855](https://github.com/pocket-id/pocket-id/issues/855)) ([7ab0fd3](https://github.com/pocket-id/pocket-id/commit/7ab0fd30286e6b67b5ce586484d82a20c42b471d))
* ignore client secret if client is public ([#836](https://github.com/pocket-id/pocket-id/issues/836)) ([7b1f6b8](https://github.com/pocket-id/pocket-id/commit/7b1f6b88572bac1f3e838a9e904917fbd5fbdf61))
* move audit log call before TX is committed ([#854](https://github.com/pocket-id/pocket-id/issues/854)) ([9339e88](https://github.com/pocket-id/pocket-id/commit/9339e88a5a26ff77a5e40149cbb1a5b339b7ec6a))
* non admin users can't revoke oidc client but see edit link ([0e44f24](https://github.com/pocket-id/pocket-id/commit/0e44f245afcdf8179bf619613ca9ef4bffa176ca))
* oidc client advanced options color ([fc0c99a](https://github.com/pocket-id/pocket-id/commit/fc0c99a232b0efb1a5b5d2c551102418b1080293))
## [](https://github.com/pocket-id/pocket-id/compare/v1.6.4...v) (2025-08-10) ## [](https://github.com/pocket-id/pocket-id/compare/v1.6.4...v) (2025-08-10)

View File

@@ -52,7 +52,7 @@ If you use [Dev Containers](https://code.visualstudio.com/docs/remote/containers
If you don't use Dev Containers, you need to install the following tools manually: If you don't use Dev Containers, you need to install the following tools manually:
- [Node.js](https://nodejs.org/en/download/) >= 22 - [Node.js](https://nodejs.org/en/download/) >= 22
- [Go](https://golang.org/doc/install) >= 1.24 - [Go](https://golang.org/doc/install) >= 1.25
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
### 2. Setup ### 2. Setup

View File

@@ -18,7 +18,7 @@ COPY ./frontend ./frontend/
RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
# Stage 2: Build Backend # Stage 2: Build Backend
FROM golang:1.24-alpine AS backend-builder FROM golang:1.25-alpine AS backend-builder
ARG BUILD_TAGS ARG BUILD_TAGS
WORKDIR /build WORKDIR /build
COPY ./backend/go.mod ./backend/go.sum ./ COPY ./backend/go.mod ./backend/go.sum ./

View File

@@ -1,34 +1,34 @@
module github.com/pocket-id/pocket-id/backend module github.com/pocket-id/pocket-id/backend
go 1.24.0 go 1.25
require ( require (
github.com/caarlos0/env/v11 v11.3.1 github.com/caarlos0/env/v11 v11.3.1
github.com/cenkalti/backoff/v5 v5.0.2 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-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.21.3 github.com/emersion/go-smtp v0.21.3
github.com/fxamacker/cbor/v2 v2.7.0 github.com/fxamacker/cbor/v2 v2.9.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.1
github.com/glebarez/go-sqlite v1.21.2 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.15.0 github.com/go-co-op/gocron/v2 v2.16.3
github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-playground/validator/v10 v10.25.0 github.com/go-playground/validator/v10 v10.27.0
github.com/go-webauthn/webauthn v0.11.2 github.com/go-webauthn/webauthn v0.11.2
github.com/golang-migrate/migrate/v4 v4.18.2 github.com/golang-migrate/migrate/v4 v4.18.3
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-beta2 github.com/lestrrat-go/httprc/v3 v3.0.0
github.com/lestrrat-go/jwx/v3 v3.0.1 github.com/lestrrat-go/jwx/v3 v3.0.10
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.2 github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
github.com/samber/slog-gin v1.15.1 github.com/samber/slog-gin v1.15.1
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
@@ -43,55 +43,56 @@ require (
go.opentelemetry.io/otel/sdk/log v0.10.0 go.opentelemetry.io/otel/sdk/log v0.10.0
go.opentelemetry.io/otel/sdk/metric v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.35.0
go.opentelemetry.io/otel/trace v1.37.0 go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/crypto v0.39.0 golang.org/x/crypto v0.41.0
golang.org/x/image v0.24.0 golang.org/x/image v0.30.0
golang.org/x/text v0.26.0 golang.org/x/text v0.28.0
golang.org/x/time v0.9.0 golang.org/x/time v0.12.0
gorm.io/driver/postgres v1.5.11 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.25.12 gorm.io/gorm v1.30.1
) )
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.12.10 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.2.3 // 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.5 // 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.1.2 // 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.8 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.0.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.7 // 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.16 // indirect github.com/go-webauthn/x v0.1.23 // 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.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/google/go-tpm v0.9.3 // indirect github.com/google/go-tpm v0.9.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // 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.2 // indirect github.com/jackc/pgx/v5 v5.7.5 // 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
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // 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.3 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // 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/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.24 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -99,7 +100,7 @@ require (
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 v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // 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.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
@@ -110,7 +111,8 @@ require (
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.3.0 // 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.1.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
@@ -127,18 +129,18 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.14.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.38.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.71.0 // indirect google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.65.10 // indirect modernc.org/libc v1.66.7 // 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.0 // indirect modernc.org/sqlite v1.38.2 // indirect
) )

View File

@@ -8,30 +8,28 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
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/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/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.3/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=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
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.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
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/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg= github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
@@ -54,22 +52,22 @@ github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGV
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/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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 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-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo= github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig= github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
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-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=
@@ -83,27 +81,27 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
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/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8= github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us= github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
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/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.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.5/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=
@@ -127,8 +125,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
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.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/pgx/v5 v5.7.5/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=
@@ -157,10 +155,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -169,16 +165,18 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
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-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY= github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE= github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/jwx/v3 v3.0.1 h1:fH3T748FCMbXoF9UXXNS9i0q6PpYyJZK/rKSbkt2guY= github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.1/go.mod h1:XP2WqxMOSzHSyf3pfibCcfsLqbomxakAnNqiuaH8nwo= github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
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/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
@@ -212,10 +210,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU= github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
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.2 h1:jG+FaCBv3h6GD5F+oenTfe3+0NmX8sCKjni5k3A5Dek= 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.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M= github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -246,7 +244,6 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -254,13 +251,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.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/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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
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=
@@ -318,8 +316,8 @@ 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=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
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-20210921155107-089bfa567519/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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
@@ -327,20 +325,20 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
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.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
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=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
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=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -352,8 +350,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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=
@@ -361,8 +359,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
@@ -375,8 +373,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@@ -395,18 +393,18 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
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=
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=
@@ -414,8 +412,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:
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/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/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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=
@@ -423,20 +421,22 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
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/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA= modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
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/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= 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/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
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=
@@ -445,10 +445,9 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 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=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -86,9 +86,6 @@ func connectDatabase() (db *gorm.DB, err error) {
if common.EnvConfig.DbConnectionString == "" { if common.EnvConfig.DbConnectionString == "" {
return nil, errors.New("missing required env var 'DB_CONNECTION_STRING' for SQLite database") return nil, errors.New("missing required env var 'DB_CONNECTION_STRING' for SQLite database")
} }
if !strings.HasPrefix(common.EnvConfig.DbConnectionString, "file:") {
return nil, errors.New("invalid value for env var 'DB_CONNECTION_STRING': does not begin with 'file:'")
}
sqliteutil.RegisterSqliteFunctions() sqliteutil.RegisterSqliteFunctions()
connString, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString) connString, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
if err != nil { if err != nil {
@@ -123,25 +120,43 @@ func connectDatabase() (db *gorm.DB, err error) {
return nil, err return nil, err
} }
// The official C implementation of SQLite allows some additional properties in the connection string
// that are not supported in the in the modernc.org/sqlite driver, and which must be passed as PRAGMA args instead.
// To ensure that people can use similar args as in the C driver, which was also used by Pocket ID
// previously (via github.com/mattn/go-sqlite3), we are converting some options.
func parseSqliteConnectionString(connString string) (string, error) { func parseSqliteConnectionString(connString string) (string, error) {
if !strings.HasPrefix(connString, "file:") { if !strings.HasPrefix(connString, "file:") {
connString = "file:" + connString connString = "file:" + connString
} }
// Check if we're using an in-memory database
isMemoryDB := isSqliteInMemory(connString)
// Parse the connection string
connStringUrl, err := url.Parse(connString) connStringUrl, err := url.Parse(connString)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse SQLite connection string: %w", err) return "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
} }
// Convert options for the old SQLite driver to the new one
convertSqlitePragmaArgs(connStringUrl)
// Add the default and required params
err = addSqliteDefaultParameters(connStringUrl, isMemoryDB)
if err != nil {
return "", fmt.Errorf("invalid SQLite connection string: %w", err)
}
return connStringUrl.String(), nil
}
// The official C implementation of SQLite allows some additional properties in the connection string
// that are not supported in the in the modernc.org/sqlite driver, and which must be passed as PRAGMA args instead.
// To ensure that people can use similar args as in the C driver, which was also used by Pocket ID
// previously (via github.com/mattn/go-sqlite3), we are converting some options.
// Note this function updates connStringUrl.
func convertSqlitePragmaArgs(connStringUrl *url.URL) {
// Reference: https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string // Reference: https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string
// This only includes a subset of options, excluding those that are not relevant to us // This only includes a subset of options, excluding those that are not relevant to us
qs := make(url.Values, len(connStringUrl.Query())) qs := make(url.Values, len(connStringUrl.Query()))
for k, v := range connStringUrl.Query() { for k, v := range connStringUrl.Query() {
switch k { switch strings.ToLower(k) {
case "_auto_vacuum", "_vacuum": case "_auto_vacuum", "_vacuum":
qs.Add("_pragma", "auto_vacuum("+v[0]+")") qs.Add("_pragma", "auto_vacuum("+v[0]+")")
case "_busy_timeout", "_timeout": case "_busy_timeout", "_timeout":
@@ -162,9 +177,123 @@ func parseSqliteConnectionString(connString string) (string, error) {
} }
} }
// Update the connStringUrl object
connStringUrl.RawQuery = qs.Encode()
}
// Adds the default (and some required) parameters to the SQLite connection string.
// Note this function updates connStringUrl.
func addSqliteDefaultParameters(connStringUrl *url.URL, isMemoryDB bool) error {
// This function include code adapted from https://github.com/dapr/components-contrib/blob/v1.14.6/
// Copyright (C) 2023 The Dapr Authors
// License: Apache2
const defaultBusyTimeout = 2500 * time.Millisecond
// Get the "query string" from the connection string if present
qs := connStringUrl.Query()
if len(qs) == 0 {
qs = make(url.Values, 2)
}
// If the database is in-memory, we must ensure that cache=shared is set
if isMemoryDB {
qs["cache"] = []string{"shared"}
}
// Check if the database is read-only or immutable
isReadOnly := false
if len(qs["mode"]) > 0 {
// Keep the first value only
qs["mode"] = []string{
strings.ToLower(qs["mode"][0]),
}
if qs["mode"][0] == "ro" {
isReadOnly = true
}
}
if len(qs["immutable"]) > 0 {
// Keep the first value only
qs["immutable"] = []string{
strings.ToLower(qs["immutable"][0]),
}
if qs["immutable"][0] == "1" {
isReadOnly = true
}
}
// We do not want to override a _txlock if set, but we'll show a warning if it's not "immediate"
if len(qs["_txlock"]) > 0 {
// Keep the first value only
qs["_txlock"] = []string{
strings.ToLower(qs["_txlock"][0]),
}
if qs["_txlock"][0] != "immediate" {
slog.Warn("SQLite connection is being created with a _txlock different from the recommended value 'immediate'")
}
} else {
qs["_txlock"] = []string{"immediate"}
}
// Add pragma values
var hasBusyTimeout, hasJournalMode bool
if len(qs["_pragma"]) == 0 {
qs["_pragma"] = make([]string, 0, 3)
} else {
for _, p := range qs["_pragma"] {
p = strings.ToLower(p)
switch {
case strings.HasPrefix(p, "busy_timeout"):
hasBusyTimeout = true
case strings.HasPrefix(p, "journal_mode"):
hasJournalMode = true
case strings.HasPrefix(p, "foreign_keys"):
return errors.New("found forbidden option '_pragma=foreign_keys' in the connection string")
}
}
}
if !hasBusyTimeout {
qs["_pragma"] = append(qs["_pragma"], fmt.Sprintf("busy_timeout(%d)", defaultBusyTimeout.Milliseconds()))
}
if !hasJournalMode {
switch {
case isMemoryDB:
// For in-memory databases, set the journal to MEMORY, the only allowed option besides OFF (which would make transactions ineffective)
qs["_pragma"] = append(qs["_pragma"], "journal_mode(MEMORY)")
case isReadOnly:
// Set the journaling mode to "DELETE" (the default) if the database is read-only
qs["_pragma"] = append(qs["_pragma"], "journal_mode(DELETE)")
default:
// Enable WAL
qs["_pragma"] = append(qs["_pragma"], "journal_mode(WAL)")
}
}
// Forcefully enable foreign keys
qs["_pragma"] = append(qs["_pragma"], "foreign_keys(1)")
// Update the connStringUrl object
connStringUrl.RawQuery = qs.Encode() connStringUrl.RawQuery = qs.Encode()
return connStringUrl.String(), nil return nil
}
// isSqliteInMemory returns true if the connection string is for an in-memory database.
func isSqliteInMemory(connString string) bool {
lc := strings.ToLower(connString)
// First way to define an in-memory database is to use ":memory:" or "file::memory:" as connection string
if strings.HasPrefix(lc, ":memory:") || strings.HasPrefix(lc, "file::memory:") {
return true
}
// Another way is to pass "mode=memory" in the "query string"
idx := strings.IndexRune(lc, '?')
if idx < 0 {
return false
}
qs, _ := url.ParseQuery(lc[(idx + 1):])
return len(qs["mode"]) > 0 && qs["mode"][0] == "memory"
} }
func getGormLogger() gormLogger.Interface { func getGormLogger() gormLogger.Interface {

View File

@@ -8,23 +8,93 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestParseSqliteConnectionString(t *testing.T) { func TestIsSqliteInMemory(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string connStr string
expected string expected bool
expectedError bool }{
{
name: "memory database with :memory:",
connStr: ":memory:",
expected: true,
},
{
name: "memory database with file::memory:",
connStr: "file::memory:",
expected: true,
},
{
name: "memory database with :MEMORY: (uppercase)",
connStr: ":MEMORY:",
expected: true,
},
{
name: "memory database with FILE::MEMORY: (uppercase)",
connStr: "FILE::MEMORY:",
expected: true,
},
{
name: "memory database with mixed case",
connStr: ":Memory:",
expected: true,
},
{
name: "has mode=memory",
connStr: "file:data?mode=memory",
expected: true,
},
{
name: "file database",
connStr: "data.db",
expected: false,
},
{
name: "file database with path",
connStr: "/path/to/data.db",
expected: false,
},
{
name: "file database with file: prefix",
connStr: "file:data.db",
expected: false,
},
{
name: "empty string",
connStr: "",
expected: false,
},
{
name: "string containing memory but not at start",
connStr: "data:memory:.db",
expected: false,
},
{
name: "has mode=ro",
connStr: "file:data?mode=ro",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isSqliteInMemory(tt.connStr)
assert.Equal(t, tt.expected, result)
})
}
}
func TestConvertSqlitePragmaArgs(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{ }{
{ {
name: "basic file path", name: "basic file path",
input: "file:test.db", input: "file:test.db",
expected: "file:test.db", expected: "file:test.db",
}, },
{
name: "adds file: prefix if missing",
input: "test.db",
expected: "file:test.db",
},
{ {
name: "converts _busy_timeout to pragma", name: "converts _busy_timeout to pragma",
input: "file:test.db?_busy_timeout=5000", input: "file:test.db?_busy_timeout=5000",
@@ -100,46 +170,161 @@ func TestParseSqliteConnectionString(t *testing.T) {
input: "file:test.db?_fk=1&mode=rw&_timeout=5000", input: "file:test.db?_fk=1&mode=rw&_timeout=5000",
expected: "file:test.db?_pragma=foreign_keys%281%29&_pragma=busy_timeout%285000%29&mode=rw", expected: "file:test.db?_pragma=foreign_keys%281%29&_pragma=busy_timeout%285000%29&mode=rw",
}, },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resultURL, _ := url.Parse(tt.input)
convertSqlitePragmaArgs(resultURL)
// Parse both URLs to compare components independently
expectedURL, err := url.Parse(tt.expected)
require.NoError(t, err)
// Compare scheme and path components
compareQueryStrings(t, expectedURL, resultURL)
})
}
}
func TestAddSqliteDefaultParameters(t *testing.T) {
tests := []struct {
name string
input string
isMemoryDB bool
expected string
expectError bool
}{
{ {
name: "invalid URL format", name: "basic file database",
input: "file:invalid#$%^&*@test.db", input: "file:test.db",
expectedError: true, isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate",
},
{
name: "in-memory database",
input: "file::memory:",
isMemoryDB: true,
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate&cache=shared",
},
{
name: "read-only database with mode=ro",
input: "file:test.db?mode=ro",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&mode=ro",
},
{
name: "immutable database",
input: "file:test.db?immutable=1",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&immutable=1",
},
{
name: "database with existing _txlock",
input: "file:test.db?_txlock=deferred",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=deferred",
},
{
name: "database with existing busy_timeout pragma",
input: "file:test.db?_pragma=busy_timeout%285000%29",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%285000%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate",
},
{
name: "database with existing journal_mode pragma",
input: "file:test.db?_pragma=journal_mode%28DELETE%29",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate",
},
{
name: "database with forbidden foreign_keys pragma",
input: "file:test.db?_pragma=foreign_keys%280%29",
isMemoryDB: false,
expectError: true,
},
{
name: "database with multiple existing pragmas",
input: "file:test.db?_pragma=busy_timeout%283000%29&_pragma=journal_mode%28TRUNCATE%29&_pragma=synchronous%28NORMAL%29",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%283000%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28TRUNCATE%29&_pragma=synchronous%28NORMAL%29&_txlock=immediate",
},
{
name: "in-memory database with cache already set",
input: "file::memory:?cache=private",
isMemoryDB: true,
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate&cache=shared",
},
{
name: "database with mode=rw (not read-only)",
input: "file:test.db?mode=rw",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate&mode=rw",
},
{
name: "database with immutable=0 (not immutable)",
input: "file:test.db?immutable=0",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate&immutable=0",
},
{
name: "database with mixed case mode=RO",
input: "file:test.db?mode=RO",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&mode=ro",
},
{
name: "database with mixed case immutable=1",
input: "file:test.db?immutable=1",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&immutable=1",
},
{
name: "complex database configuration",
input: "file:test.db?cache=shared&mode=rwc&_txlock=immediate&_pragma=synchronous%28FULL%29",
isMemoryDB: false,
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_pragma=synchronous%28FULL%29&_txlock=immediate&cache=shared&mode=rwc",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := parseSqliteConnectionString(tt.input) resultURL, err := url.Parse(tt.input)
require.NoError(t, err)
if tt.expectedError { err = addSqliteDefaultParameters(resultURL, tt.isMemoryDB)
if tt.expectError {
require.Error(t, err) require.Error(t, err)
return return
} }
require.NoError(t, err) require.NoError(t, err)
// Parse both URLs to compare components independently
expectedURL, err := url.Parse(tt.expected) expectedURL, err := url.Parse(tt.expected)
require.NoError(t, err) require.NoError(t, err)
resultURL, err := url.Parse(result) compareQueryStrings(t, expectedURL, resultURL)
require.NoError(t, err)
// Compare scheme and path components
assert.Equal(t, expectedURL.Scheme, resultURL.Scheme)
assert.Equal(t, expectedURL.Path, resultURL.Path)
// Compare query parameters regardless of order
expectedQuery := expectedURL.Query()
resultQuery := resultURL.Query()
assert.Len(t, expectedQuery, len(resultQuery))
for key, expectedValues := range expectedQuery {
resultValues, ok := resultQuery[key]
_ = assert.True(t, ok) &&
assert.ElementsMatch(t, expectedValues, resultValues)
}
}) })
} }
} }
func compareQueryStrings(t *testing.T, expectedURL *url.URL, resultURL *url.URL) {
t.Helper()
// Compare scheme and path components
assert.Equal(t, expectedURL.Scheme, resultURL.Scheme)
assert.Equal(t, expectedURL.Path, resultURL.Path)
// Compare query parameters regardless of order
expectedQuery := expectedURL.Query()
resultQuery := resultURL.Query()
assert.Len(t, expectedQuery, len(resultQuery))
for key, expectedValues := range expectedQuery {
resultValues, ok := resultQuery[key]
_ = assert.True(t, ok) &&
assert.ElementsMatch(t, expectedValues, resultValues)
}
}

View File

@@ -140,7 +140,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
addr = common.EnvConfig.UnixSocket addr = common.EnvConfig.UnixSocket
} }
listener, err := net.Listen(network, addr) listener, err := net.Listen(network, addr) //nolint:noctx
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create %s listener: %w", network, err) return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
} }

View File

@@ -46,22 +46,21 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client) (sv
return nil, fmt.Errorf("failed to create JWT service: %w", err) return nil, fmt.Errorf("failed to create JWT service: %w", err)
} }
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
svc.customClaimService = service.NewCustomClaimService(db) svc.customClaimService = service.NewCustomClaimService(db)
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService)
if err != nil {
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
}
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
svc.webauthnService, err = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService) svc.webauthnService, err = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err) return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
} }
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService)
if err != nil {
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
}
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService)
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
return svc, nil return svc, nil
} }

View File

@@ -51,7 +51,7 @@ var oneTimeAccessTokenCmd = &cobra.Command{
} }
// Create a new access token that expires in 1 hour // Create a new access token that expires in 1 hour
oneTimeAccessToken, txErr = service.NewOneTimeAccessToken(user.ID, time.Now().Add(time.Hour)) oneTimeAccessToken, txErr = service.NewOneTimeAccessToken(user.ID, time.Hour)
if txErr != nil { if txErr != nil {
return fmt.Errorf("failed to generate access token: %w", txErr) return fmt.Errorf("failed to generate access token: %w", txErr)
} }

View File

@@ -26,7 +26,7 @@ const (
DbProviderSqlite DbProvider = "sqlite" DbProviderSqlite DbProvider = "sqlite"
DbProviderPostgres DbProvider = "postgres" DbProviderPostgres DbProvider = "postgres"
MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz" MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz"
defaultSqliteConnString string = "file:data/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate" defaultSqliteConnString string = "data/pocket-id.db"
) )
type EnvConfigSchema struct { type EnvConfigSchema struct {

View File

@@ -350,6 +350,15 @@ func (e *OidcAuthorizationPendingError) HttpStatusCode() int {
return http.StatusBadRequest return http.StatusBadRequest
} }
type ReauthenticationRequiredError struct{}
func (e *ReauthenticationRequiredError) Error() string {
return "reauthentication required"
}
func (e *ReauthenticationRequiredError) HttpStatusCode() int {
return http.StatusUnauthorized
}
type OpenSignupDisabledError struct{} type OpenSignupDisabledError struct{}
func (e *OpenSignupDisabledError) Error() string { func (e *OpenSignupDisabledError) Error() string {
@@ -359,3 +368,13 @@ func (e *OpenSignupDisabledError) Error() string {
func (e *OpenSignupDisabledError) HttpStatusCode() int { func (e *OpenSignupDisabledError) HttpStatusCode() int {
return http.StatusForbidden return http.StatusForbidden
} }
type ClientIdAlreadyExistsError struct{}
func (e *ClientIdAlreadyExistsError) Error() string {
return "Client ID already in use"
}
func (e *ClientIdAlreadyExistsError) HttpStatusCode() int {
return http.StatusBadRequest
}

View File

@@ -55,10 +55,12 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
group.POST("/oidc/device/verify", authMiddleware.WithAdminNotRequired().Add(), oc.verifyDeviceCodeHandler) group.POST("/oidc/device/verify", authMiddleware.WithAdminNotRequired().Add(), oc.verifyDeviceCodeHandler)
group.GET("/oidc/device/info", authMiddleware.WithAdminNotRequired().Add(), oc.getDeviceCodeInfoHandler) group.GET("/oidc/device/info", authMiddleware.WithAdminNotRequired().Add(), oc.getDeviceCodeInfoHandler)
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler) group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
group.GET("/oidc/users/:id/clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler) group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
group.DELETE("/oidc/users/me/clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler) group.DELETE("/oidc/users/me/authorized-clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler)
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAccessibleClientsHandler)
} }
@@ -490,11 +492,11 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "Client ID" // @Param id path string true "Client ID"
// @Param client body dto.OidcClientCreateDto true "Client information" // @Param client body dto.OidcClientUpdateDto true "Client information"
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client" // @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client"
// @Router /api/oidc/clients/{id} [put] // @Router /api/oidc/clients/{id} [put]
func (oc *OidcController) updateClientHandler(c *gin.Context) { func (oc *OidcController) updateClientHandler(c *gin.Context) {
var input dto.OidcClientCreateDto var input dto.OidcClientUpdateDto
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -660,7 +662,7 @@ func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
// @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")
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto] // @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
// @Router /api/oidc/users/me/clients [get] // @Router /api/oidc/users/me/authorized-clients [get]
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) { func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
userID := c.GetString("userID") userID := c.GetString("userID")
oc.listAuthorizedClients(c, userID) oc.listAuthorizedClients(c, userID)
@@ -676,7 +678,7 @@ func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
// @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")
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto] // @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
// @Router /api/oidc/users/{id}/clients [get] // @Router /api/oidc/users/{id}/authorized-clients [get]
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) { func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
userID := c.Param("id") userID := c.Param("id")
oc.listAuthorizedClients(c, userID) oc.listAuthorizedClients(c, userID)
@@ -713,7 +715,7 @@ func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
// @Tags OIDC // @Tags OIDC
// @Param clientId path string true "Client ID to revoke authorization for" // @Param clientId path string true "Client ID to revoke authorization for"
// @Success 204 "No Content" // @Success 204 "No Content"
// @Router /api/oidc/users/me/clients/{clientId} [delete] // @Router /api/oidc/users/me/authorized-clients/{clientId} [delete]
func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) { func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
clientID := c.Param("clientId") clientID := c.Param("clientId")
@@ -728,6 +730,37 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// listOwnAccessibleClientsHandler godoc
// @Summary List accessible OIDC clients for current user
// @Description Get a list of OIDC clients that the current user can access
// @Tags OIDC
// @Param pagination[page] query int false "Page number for pagination" default(1)
// @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[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
// @Router /api/oidc/users/me/clients [get]
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
userID := c.GetString("userID")
var sortedPaginationRequest utils.SortedPaginationRequest
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 {
_ = c.Error(err)
return
}
c.JSON(http.StatusOK, dto.Paginated[dto.AccessibleOidcClientDto]{
Data: clients,
Pagination: pagination,
})
}
func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) { func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) {
userCode := c.Query("code") userCode := c.Query("code")
if userCode == "" { if userCode == "" {

View File

@@ -14,6 +14,11 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
const (
defaultOneTimeAccessTokenDuration = 15 * time.Minute
defaultSignupTokenDuration = time.Hour
)
// NewUserController creates a new controller for user management endpoints // NewUserController creates a new controller for user management endpoints
// @Summary User management controller // @Summary User management controller
// @Description Initializes all user-related API endpoints // @Description Initializes all user-related API endpoints
@@ -331,10 +336,17 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bo
return return
} }
var ttl time.Duration
if own { if own {
input.UserID = c.GetString("userID") input.UserID = c.GetString("userID")
ttl = defaultOneTimeAccessTokenDuration
} else {
ttl = input.TTL.Duration
if ttl <= 0 {
ttl = defaultOneTimeAccessTokenDuration
}
} }
token, err := uc.userService.CreateOneTimeAccessToken(c.Request.Context(), input.UserID, input.ExpiresAt) token, err := uc.userService.CreateOneTimeAccessToken(c.Request.Context(), input.UserID, ttl)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -411,7 +423,11 @@ func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context
userID := c.Param("id") userID := c.Param("id")
err := uc.userService.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), userID, input.ExpiresAt) ttl := input.TTL.Duration
if ttl <= 0 {
ttl = defaultOneTimeAccessTokenDuration
}
err := uc.userService.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), userID, ttl)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
@@ -526,14 +542,20 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
return return
} }
signupToken, err := uc.userService.CreateSignupToken(c.Request.Context(), input.ExpiresAt, input.UsageLimit) ttl := input.TTL.Duration
if ttl <= 0 {
ttl = defaultSignupTokenDuration
}
signupToken, err := uc.userService.CreateSignupToken(c.Request.Context(), ttl, input.UsageLimit)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
var tokenDto dto.SignupTokenDto var tokenDto dto.SignupTokenDto
if err := dto.MapStruct(signupToken, &tokenDto); err != nil { err = dto.MapStruct(signupToken, &tokenDto)
if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }

View File

@@ -25,6 +25,8 @@ func NewWebauthnController(group *gin.RouterGroup, authMiddleware *middleware.Au
group.POST("/webauthn/logout", authMiddleware.WithAdminNotRequired().Add(), wc.logoutHandler) group.POST("/webauthn/logout", authMiddleware.WithAdminNotRequired().Add(), wc.logoutHandler)
group.POST("/webauthn/reauthenticate", authMiddleware.WithAdminNotRequired().Add(), rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), wc.reauthenticateHandler)
group.GET("/webauthn/credentials", authMiddleware.WithAdminNotRequired().Add(), wc.listCredentialsHandler) group.GET("/webauthn/credentials", authMiddleware.WithAdminNotRequired().Add(), wc.listCredentialsHandler)
group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler) group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler)
group.DELETE("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.deleteCredentialHandler) group.DELETE("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.deleteCredentialHandler)
@@ -171,3 +173,33 @@ func (wc *WebauthnController) logoutHandler(c *gin.Context) {
cookie.AddAccessTokenCookie(c, 0, "") cookie.AddAccessTokenCookie(c, 0, "")
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
func (wc *WebauthnController) reauthenticateHandler(c *gin.Context) {
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
if err != nil {
_ = c.Error(&common.MissingSessionIdError{})
return
}
var token string
// Try to create a reauthentication token with WebAuthn
credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body)
if err == nil {
token, err = wc.webAuthnService.CreateReauthenticationTokenWithWebauthn(c.Request.Context(), sessionID, credentialAssertionData)
if err != nil {
_ = c.Error(err)
return
}
} else {
// If WebAuthn fails, try to create a reauthentication token with the access token
accessToken, _ := c.Cookie(cookie.AccessTokenCookieName)
token, err = wc.webAuthnService.CreateReauthenticationTokenWithAccessToken(c.Request.Context(), accessToken)
if err != nil {
_ = c.Error(err)
return
}
}
c.JSON(http.StatusOK, gin.H{"reauthenticationToken": token})
}

View File

@@ -18,6 +18,8 @@ type AppConfigUpdateDto struct {
DisableAnimations string `json:"disableAnimations" binding:"required"` DisableAnimations string `json:"disableAnimations" binding:"required"`
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"` AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
AllowUserSignups string `json:"allowUserSignups" binding:"required,oneof=disabled withToken open"` AllowUserSignups string `json:"allowUserSignups" binding:"required,oneof=disabled withToken open"`
SignupDefaultUserGroupIDs string `json:"signupDefaultUserGroupIDs" binding:"omitempty,json"`
SignupDefaultCustomClaims string `json:"signupDefaultCustomClaims" binding:"omitempty,json"`
AccentColor string `json:"accentColor"` AccentColor string `json:"accentColor"`
SmtpHost string `json:"smtpHost"` SmtpHost string `json:"smtpHost"`
SmtpPort string `json:"smtpPort"` SmtpPort string `json:"smtpPort"`

View File

@@ -3,10 +3,11 @@ package dto
import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
type OidcClientMetaDataDto struct { 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"`
LaunchURL *string `json:"launchURL"` LaunchURL *string `json:"launchURL"`
RequiresReauthentication bool `json:"requiresReauthentication"`
} }
type OidcClientDto struct { type OidcClientDto struct {
@@ -28,14 +29,20 @@ type OidcClientWithAllowedGroupsCountDto struct {
AllowedUserGroupsCount int64 `json:"allowedUserGroupsCount"` AllowedUserGroupsCount int64 `json:"allowedUserGroupsCount"`
} }
type OidcClientUpdateDto struct {
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
CallbackURLs []string `json:"callbackURLs"`
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled"`
RequiresReauthentication bool `json:"requiresReauthentication"`
Credentials OidcClientCredentialsDto `json:"credentials"`
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
}
type OidcClientCreateDto struct { type OidcClientCreateDto struct {
Name string `json:"name" binding:"required,max=50" unorm:"nfc"` OidcClientUpdateDto
CallbackURLs []string `json:"callbackURLs"` ID string `json:"id" binding:"omitempty,client_id,min=2,max=128"`
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled"`
Credentials OidcClientCredentialsDto `json:"credentials"`
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
} }
type OidcClientCredentialsDto struct { type OidcClientCredentialsDto struct {
@@ -50,12 +57,13 @@ type OidcClientFederatedIdentityDto struct {
} }
type AuthorizeOidcClientRequestDto struct { type AuthorizeOidcClientRequestDto struct {
ClientID string `json:"clientID" binding:"required"` ClientID string `json:"clientID" binding:"required"`
Scope string `json:"scope" binding:"required"` Scope string `json:"scope" binding:"required"`
CallbackURL string `json:"callbackURL"` CallbackURL string `json:"callbackURL"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
CodeChallenge string `json:"codeChallenge"` CodeChallenge string `json:"codeChallenge"`
CodeChallengeMethod string `json:"codeChallengeMethod"` CodeChallengeMethod string `json:"codeChallengeMethod"`
ReauthenticationToken string `json:"reauthenticationToken"`
} }
type AuthorizeOidcClientResponseDto struct { type AuthorizeOidcClientResponseDto struct {
@@ -159,3 +167,8 @@ type OidcClientPreviewDto struct {
AccessToken map[string]any `json:"accessToken"` AccessToken map[string]any `json:"accessToken"`
UserInfo map[string]any `json:"userInfo"` UserInfo map[string]any `json:"userInfo"`
} }
type AccessibleOidcClientDto struct {
OidcClientMetaDataDto
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
}

View File

@@ -1,14 +1,13 @@
package dto package dto
import ( import (
"time"
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 SignupTokenCreateDto struct { type SignupTokenCreateDto struct {
ExpiresAt time.Time `json:"expiresAt" binding:"required"` TTL utils.JSONDuration `json:"ttl" binding:"required,ttl"`
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"` UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
} }
type SignupTokenDto struct { type SignupTokenDto struct {

View File

@@ -1,7 +1,7 @@
package dto package dto
import ( import (
"time" "github.com/pocket-id/pocket-id/backend/internal/utils"
) )
type UserDto struct { type UserDto struct {
@@ -30,8 +30,8 @@ type UserCreateDto struct {
} }
type OneTimeAccessTokenCreateDto struct { type OneTimeAccessTokenCreateDto struct {
UserID string `json:"userId"` UserID string `json:"userId"`
ExpiresAt time.Time `json:"expiresAt" binding:"required"` TTL utils.JSONDuration `json:"ttl" binding:"ttl"`
} }
type OneTimeAccessEmailAsUnauthenticatedUserDto struct { type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
@@ -40,7 +40,7 @@ type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
} }
type OneTimeAccessEmailAsAdminDto struct { type OneTimeAccessEmailAsAdminDto struct {
ExpiresAt time.Time `json:"expiresAt" binding:"required"` TTL utils.JSONDuration `json:"ttl" binding:"ttl"`
} }
type UserUpdateUserGroupDto struct { type UserUpdateUserGroupDto struct {

View File

@@ -1,29 +1,52 @@
package dto package dto
import ( import (
"log/slog"
"os"
"regexp" "regexp"
"time"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )
// [a-zA-Z0-9] : The username must start with an alphanumeric character
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.@-]*[a-zA-Z0-9]$")
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
return validateUsernameRegex.MatchString(fl.Field().String())
}
func init() { func init() {
v, _ := binding.Validator.Engine().(*validator.Validate) v := binding.Validator.Engine().(*validator.Validate)
err := v.RegisterValidation("username", validateUsername)
// [a-zA-Z0-9] : The username must start with an alphanumeric character
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.@-]*[a-zA-Z0-9]$")
var validateClientIDRegex = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
// Maximum allowed value for TTLs
const maxTTL = 31 * 24 * time.Hour
// Errors here are development-time ones
err := v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
return validateUsernameRegex.MatchString(fl.Field().String())
})
if err != nil { if err != nil {
slog.Error("Failed to register custom validation", slog.Any("error", err)) panic("Failed to register custom validation for username: " + err.Error())
os.Exit(1) }
return
err = v.RegisterValidation("client_id", func(fl validator.FieldLevel) bool {
return validateClientIDRegex.MatchString(fl.Field().String())
})
if err != nil {
panic("Failed to register custom validation for client_id: " + err.Error())
}
err = v.RegisterValidation("ttl", func(fl validator.FieldLevel) bool {
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
if !ok {
return false
}
// Allow zero, which means the field wasn't set
return ttl.Duration == 0 || ttl.Duration > time.Second && ttl.Duration <= maxTTL
})
if err != nil {
panic("Failed to register custom validation for ttl: " + err.Error())
} }
} }

View File

@@ -25,6 +25,7 @@ func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) erro
s.registerJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true), s.registerJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true),
s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true), s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true),
s.registerJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true), s.registerJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true),
s.registerJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true),
s.registerJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true), s.registerJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true),
) )
} }
@@ -104,6 +105,20 @@ func (j *DbCleanupJobs) clearOidcRefreshTokens(ctx context.Context) error {
return nil return nil
} }
// ClearReauthenticationTokens deletes reauthentication tokens that have expired
func (j *DbCleanupJobs) clearReauthenticationTokens(ctx context.Context) error {
st := j.db.
WithContext(ctx).
Delete(&model.ReauthenticationToken{}, "expires_at < ?", datatype.DateTime(time.Now()))
if st.Error != nil {
return fmt.Errorf("failed to clean expired reauthentication tokens: %w", st.Error)
}
slog.InfoContext(ctx, "Cleaned expired reauthentication tokens", slog.Int64("count", st.RowsAffected))
return nil
}
// ClearAuditLogs deletes audit logs older than 90 days // ClearAuditLogs deletes audit logs older than 90 days
func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error { func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
st := j.db. st := j.db.

View File

@@ -34,13 +34,15 @@ func (a *AppConfigVariable) AsDurationMinutes() time.Duration {
type AppConfig struct { type AppConfig struct {
// General // General
AppName AppConfigVariable `key:"appName,public"` // Public AppName AppConfigVariable `key:"appName,public"` // Public
SessionDuration AppConfigVariable `key:"sessionDuration"` SessionDuration AppConfigVariable `key:"sessionDuration"`
EmailsVerified AppConfigVariable `key:"emailsVerified"` EmailsVerified AppConfigVariable `key:"emailsVerified"`
AccentColor AppConfigVariable `key:"accentColor,public"` // Public AccentColor AppConfigVariable `key:"accentColor,public"` // Public
DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public
AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
AllowUserSignups AppConfigVariable `key:"allowUserSignups,public"` // Public AllowUserSignups AppConfigVariable `key:"allowUserSignups,public"` // Public
SignupDefaultUserGroupIDs AppConfigVariable `key:"signupDefaultUserGroupIDs"`
SignupDefaultCustomClaims AppConfigVariable `key:"signupDefaultCustomClaims"`
// Internal // Internal
BackgroundImageType AppConfigVariable `key:"backgroundImageType,internal"` // Internal BackgroundImageType AppConfigVariable `key:"backgroundImageType,internal"` // Internal
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal

View File

@@ -40,20 +40,22 @@ type OidcAuthorizationCode struct {
type OidcClient struct { type OidcClient struct {
Base Base
Name string `sortable:"true"` Name string `sortable:"true"`
Secret string Secret string
CallbackURLs UrlList CallbackURLs UrlList
LogoutCallbackURLs UrlList LogoutCallbackURLs UrlList
ImageType *string ImageType *string
HasLogo bool `gorm:"-"` HasLogo bool `gorm:"-"`
IsPublic bool IsPublic bool
PkceEnabled bool PkceEnabled bool
Credentials OidcClientCredentials RequiresReauthentication bool
LaunchURL *string Credentials OidcClientCredentials
LaunchURL *string
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"` AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
CreatedByID string CreatedByID *string
CreatedBy User CreatedBy *User
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
} }
type OidcRefreshToken struct { type OidcRefreshToken struct {

View File

@@ -45,6 +45,15 @@ type PublicKeyCredentialRequestOptions struct {
Timeout time.Duration Timeout time.Duration
} }
type ReauthenticationToken struct {
Base
Token string
ExpiresAt datatype.DateTime
UserID string
User User
}
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
// Scan and Value methods for GORM to handle the custom type // Scan and Value methods for GORM to handle the custom type

View File

@@ -60,13 +60,15 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
// Values are the default ones // Values are the default ones
return &model.AppConfig{ return &model.AppConfig{
// General // General
AppName: model.AppConfigVariable{Value: "Pocket ID"}, AppName: model.AppConfigVariable{Value: "Pocket ID"},
SessionDuration: model.AppConfigVariable{Value: "60"}, SessionDuration: model.AppConfigVariable{Value: "60"},
EmailsVerified: model.AppConfigVariable{Value: "false"}, EmailsVerified: model.AppConfigVariable{Value: "false"},
DisableAnimations: model.AppConfigVariable{Value: "false"}, DisableAnimations: model.AppConfigVariable{Value: "false"},
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"}, AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
AllowUserSignups: model.AppConfigVariable{Value: "disabled"}, AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
AccentColor: model.AppConfigVariable{Value: "default"}, SignupDefaultUserGroupIDs: model.AppConfigVariable{Value: "[]"},
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
AccentColor: model.AppConfigVariable{Value: "default"},
// Internal // Internal
BackgroundImageType: model.AppConfigVariable{Value: "jpg"}, BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
LogoLightImageType: model.AppConfigVariable{Value: "svg"}, LogoLightImageType: model.AppConfigVariable{Value: "svg"},

View File

@@ -55,16 +55,46 @@ const (
// UpdateCustomClaimsForUser updates the custom claims for a user // UpdateCustomClaimsForUser updates the custom claims for a user
func (s *CustomClaimService) UpdateCustomClaimsForUser(ctx context.Context, userID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) { func (s *CustomClaimService) UpdateCustomClaimsForUser(ctx context.Context, userID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
return s.updateCustomClaims(ctx, UserID, userID, claims) tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
updatedClaims, err := s.updateCustomClaimsInternal(ctx, UserID, userID, claims, tx)
if err != nil {
return nil, err
}
err = tx.Commit().Error
if err != nil {
return nil, err
}
return updatedClaims, nil
} }
// UpdateCustomClaimsForUserGroup updates the custom claims for a user group // UpdateCustomClaimsForUserGroup updates the custom claims for a user group
func (s *CustomClaimService) UpdateCustomClaimsForUserGroup(ctx context.Context, userGroupID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) { func (s *CustomClaimService) UpdateCustomClaimsForUserGroup(ctx context.Context, userGroupID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
return s.updateCustomClaims(ctx, UserGroupID, userGroupID, claims) tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
updatedClaims, err := s.updateCustomClaimsInternal(ctx, UserGroupID, userGroupID, claims, tx)
if err != nil {
return nil, err
}
err = tx.Commit().Error
if err != nil {
return nil, err
}
return updatedClaims, nil
} }
// updateCustomClaims updates the custom claims for a user or user group // updateCustomClaimsInternal updates the custom claims for a user or user group within a transaction
func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idType, value string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) { func (s *CustomClaimService) updateCustomClaimsInternal(ctx context.Context, idType idType, value string, claims []dto.CustomClaimCreateDto, tx *gorm.DB) ([]model.CustomClaim, error) {
// Check for duplicate keys in the claims slice // Check for duplicate keys in the claims slice
seenKeys := make(map[string]struct{}) seenKeys := make(map[string]struct{})
for _, claim := range claims { for _, claim := range claims {
@@ -74,11 +104,6 @@ func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idTy
seenKeys[claim.Key] = struct{}{} seenKeys[claim.Key] = struct{}{}
} }
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
var existingClaims []model.CustomClaim var existingClaims []model.CustomClaim
err := tx. err := tx.
WithContext(ctx). WithContext(ctx).
@@ -150,11 +175,6 @@ func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idTy
return nil, err return nil, err
} }
err = tx.Commit().Error
if err != nil {
return nil, err
}
return updatedClaims, nil return updatedClaims, nil
} }

View File

@@ -159,7 +159,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"}, CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"},
LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"}, LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"},
ImageType: utils.StringPointer("png"), ImageType: utils.StringPointer("png"),
CreatedByID: users[0].ID, CreatedByID: utils.Ptr(users[0].ID),
}, },
{ {
Base: model.Base{ Base: model.Base{
@@ -168,7 +168,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "Immich", Name: "Immich",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://immich/auth/callback"}, CallbackURLs: model.UrlList{"http://immich/auth/callback"},
CreatedByID: users[1].ID, CreatedByID: utils.Ptr(users[1].ID),
AllowedUserGroups: []model.UserGroup{ AllowedUserGroups: []model.UserGroup{
userGroups[1], userGroups[1],
}, },
@@ -181,7 +181,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"}, CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"}, LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
CreatedByID: users[0].ID, CreatedByID: utils.Ptr(users[0].ID),
}, },
{ {
Base: model.Base{ Base: model.Base{
@@ -190,7 +190,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "Federated", Name: "Federated",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://federated/auth/callback"}, CallbackURLs: model.UrlList{"http://federated/auth/callback"},
CreatedByID: users[1].ID, CreatedByID: utils.Ptr(users[1].ID),
AllowedUserGroups: []model.UserGroup{}, AllowedUserGroups: []model.UserGroup{},
Credentials: model.OidcClientCredentials{ Credentials: model.OidcClientCredentials{
FederatedIdentities: []model.OidcClientFederatedIdentity{ FederatedIdentities: []model.OidcClientFederatedIdentity{

View File

@@ -50,6 +50,7 @@ type OidcService struct {
appConfigService *AppConfigService appConfigService *AppConfigService
auditLogService *AuditLogService auditLogService *AuditLogService
customClaimService *CustomClaimService customClaimService *CustomClaimService
webAuthnService *WebAuthnService
httpClient *http.Client httpClient *http.Client
jwkCache *jwk.Cache jwkCache *jwk.Cache
@@ -62,6 +63,7 @@ func NewOidcService(
appConfigService *AppConfigService, appConfigService *AppConfigService,
auditLogService *AuditLogService, auditLogService *AuditLogService,
customClaimService *CustomClaimService, customClaimService *CustomClaimService,
webAuthnService *WebAuthnService,
) (s *OidcService, err error) { ) (s *OidcService, err error) {
s = &OidcService{ s = &OidcService{
db: db, db: db,
@@ -69,6 +71,7 @@ func NewOidcService(
appConfigService: appConfigService, appConfigService: appConfigService,
auditLogService: auditLogService, auditLogService: auditLogService,
customClaimService: customClaimService, customClaimService: customClaimService,
webAuthnService: webAuthnService,
} }
// Note: we don't pass the HTTP Client with OTel instrumented to this because requests are always made in background and not tied to a specific trace // Note: we don't pass the HTTP Client with OTel instrumented to this because requests are always made in background and not tied to a specific trace
@@ -123,6 +126,16 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
return "", "", err return "", "", err
} }
if client.RequiresReauthentication {
if input.ReauthenticationToken == "" {
return "", "", &common.ReauthenticationRequiredError{}
}
err = s.webAuthnService.ConsumeReauthenticationToken(ctx, tx, input.ReauthenticationToken, userID)
if err != nil {
return "", "", err
}
}
// If the client is not public, the code challenge must be provided // If the client is not public, the code challenge must be provided
if client.IsPublic && input.CodeChallenge == "" { if client.IsPublic && input.CodeChallenge == "" {
return "", "", &common.OidcMissingCodeChallengeError{} return "", "", &common.OidcMissingCodeChallengeError{}
@@ -641,8 +654,7 @@ 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
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc" if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && isValidSortDirection {
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").
@@ -658,22 +670,28 @@ func (s *OidcService) ListClients(ctx context.Context, name string, sortedPagina
func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) { func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) {
client := model.OidcClient{ client := model.OidcClient{
CreatedByID: userID, Base: model.Base{
ID: input.ID,
},
CreatedByID: utils.Ptr(userID),
} }
updateOIDCClientModelFromDto(&client, &input) updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)
err := s.db. err := s.db.
WithContext(ctx). WithContext(ctx).
Create(&client). Create(&client).
Error Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return model.OidcClient{}, &common.ClientIdAlreadyExistsError{}
}
return model.OidcClient{}, err return model.OidcClient{}, err
} }
return client, nil return client, nil
} }
func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientCreateDto) (model.OidcClient, error) { func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientUpdateDto) (model.OidcClient, error) {
tx := s.db.Begin() tx := s.db.Begin()
defer func() { defer func() {
tx.Rollback() tx.Rollback()
@@ -707,7 +725,7 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
return client, nil return client, nil
} }
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientCreateDto) { func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
// Base fields // Base fields
client.Name = input.Name client.Name = input.Name
client.CallbackURLs = input.CallbackURLs client.CallbackURLs = input.CallbackURLs
@@ -715,20 +733,20 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
client.IsPublic = input.IsPublic client.IsPublic = input.IsPublic
// PKCE is required for public clients // PKCE is required for public clients
client.PkceEnabled = input.IsPublic || input.PkceEnabled client.PkceEnabled = input.IsPublic || input.PkceEnabled
client.RequiresReauthentication = input.RequiresReauthentication
client.LaunchURL = input.LaunchURL client.LaunchURL = input.LaunchURL
// Credentials // Credentials
if len(input.Credentials.FederatedIdentities) > 0 { client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities)) for i, fi := range input.Credentials.FederatedIdentities {
for i, fi := range input.Credentials.FederatedIdentities { client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{ Issuer: fi.Issuer,
Issuer: fi.Issuer, Audience: fi.Audience,
Audience: fi.Audience, Subject: fi.Subject,
Subject: fi.Subject, JWKS: fi.JWKS,
JWKS: fi.JWKS,
}
} }
} }
} }
func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error { func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error {
@@ -1336,6 +1354,81 @@ 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) {
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
var user model.User
err := tx.
WithContext(ctx).
Preload("UserGroups").
First(&user, "id = ?", userID).
Error
if err != nil {
return nil, utils.PaginationResponse{}, err
}
userGroupIDs := make([]string, len(user.UserGroups))
for i, group := range user.UserGroups {
userGroupIDs[i] = group.ID
}
// Build the query for accessible clients
query := tx.
WithContext(ctx).
Model(&model.OidcClient{}).
Preload("UserAuthorizedOidcClients", "user_id = ?", userID).
Distinct()
// If user has no groups, only return clients with no allowed user groups
if len(userGroupIDs) == 0 {
query = query.
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
Where("oidc_clients_allowed_user_groups.oidc_client_id IS NULL")
} else {
// Return clients with no allowed user groups OR clients where user is in allowed groups
query = query.
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
Where("oidc_clients_allowed_user_groups.oidc_client_id IS NULL OR oidc_clients_allowed_user_groups.user_group_id IN (?)", userGroupIDs)
}
var clients []model.OidcClient
// Handle custom sorting for lastUsedAt column
var response utils.PaginationResponse
if sortedPaginationRequest.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
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).
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction)
}
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
if err != nil {
return nil, utils.PaginationResponse{}, err
}
dtos := make([]dto.AccessibleOidcClientDto, len(clients))
for i, client := range clients {
var lastUsedAt *datatype.DateTime
if len(client.UserAuthorizedOidcClients) > 0 {
lastUsedAt = &client.UserAuthorizedOidcClients[0].LastUsedAt
}
dtos[i] = dto.AccessibleOidcClientDto{
OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
ID: client.ID,
Name: client.Name,
LaunchURL: client.LaunchURL,
HasLogo: client.HasLogo,
},
LastUsedAt: lastUsedAt,
}
}
return dtos, response, err
}
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, tx *gorm.DB) (string, error) { func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, tx *gorm.DB) (string, error) {
refreshToken, err := utils.GenerateRandomAlphanumericString(40) refreshToken, err := utils.GenerateRandomAlphanumericString(40)
if err != nil { if err != nil {
@@ -1462,8 +1555,8 @@ func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *g
// Validate credentials based on the authentication method // Validate credentials based on the authentication method
switch { switch {
// First, if we have a client secret, we validate it // First, if we have a client secret, we validate it unless client is marked as public
case input.ClientSecret != "": case input.ClientSecret != "" && !client.IsPublic:
err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(input.ClientSecret)) err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(input.ClientSecret))
if err != nil { if err != nil {
return nil, &common.OidcClientSecretInvalidError{} return nil, &common.OidcClientSecretInvalidError{}

View File

@@ -171,8 +171,10 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
// Create the test clients // Create the test clients
// 1. Confidential client // 1. Confidential client
confidentialClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{ confidentialClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
Name: "Confidential Client", OidcClientUpdateDto: dto.OidcClientUpdateDto{
CallbackURLs: []string{"https://example.com/callback"}, Name: "Confidential Client",
CallbackURLs: []string{"https://example.com/callback"},
},
}, "test-user-id") }, "test-user-id")
require.NoError(t, err) require.NoError(t, err)
@@ -182,20 +184,24 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
// 2. Public client // 2. Public client
publicClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{ publicClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
Name: "Public Client", OidcClientUpdateDto: dto.OidcClientUpdateDto{
CallbackURLs: []string{"https://example.com/callback"}, Name: "Public Client",
IsPublic: true, CallbackURLs: []string{"https://example.com/callback"},
IsPublic: true,
},
}, "test-user-id") }, "test-user-id")
require.NoError(t, err) require.NoError(t, err)
// 3. Confidential client with federated identity // 3. Confidential client with federated identity
federatedClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{ federatedClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
Name: "Federated Client", OidcClientUpdateDto: dto.OidcClientUpdateDto{
CallbackURLs: []string{"https://example.com/callback"}, Name: "Federated Client",
CallbackURLs: []string{"https://example.com/callback"},
},
}, "test-user-id") }, "test-user-id")
require.NoError(t, err) require.NoError(t, err)
federatedClient, err = s.UpdateClient(t.Context(), federatedClient.ID, dto.OidcClientCreateDto{ federatedClient, err = s.UpdateClient(t.Context(), federatedClient.ID, dto.OidcClientUpdateDto{
Name: federatedClient.Name, Name: federatedClient.Name,
CallbackURLs: federatedClient.CallbackURLs, CallbackURLs: federatedClient.CallbackURLs,
Credentials: dto.OidcClientCredentialsDto{ Credentials: dto.OidcClientCredentialsDto{

View File

@@ -32,8 +32,7 @@ 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
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc" if sortedPaginationRequest.Sort.Column == "userCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection {
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").

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -26,20 +27,22 @@ import (
) )
type UserService struct { type UserService struct {
db *gorm.DB db *gorm.DB
jwtService *JwtService jwtService *JwtService
auditLogService *AuditLogService auditLogService *AuditLogService
emailService *EmailService emailService *EmailService
appConfigService *AppConfigService appConfigService *AppConfigService
customClaimService *CustomClaimService
} }
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService) *UserService { func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService) *UserService {
return &UserService{ return &UserService{
db: db, db: db,
jwtService: jwtService, jwtService: jwtService,
auditLogService: auditLogService, auditLogService: auditLogService,
emailService: emailService, emailService: emailService,
appConfigService: appConfigService, appConfigService: appConfigService,
customClaimService: customClaimService,
} }
} }
@@ -268,9 +271,53 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
} else if err != nil { } else if err != nil {
return model.User{}, err return model.User{}, err
} }
// Apply default groups and claims for new non-LDAP users
if !isLdapSync {
if err := s.applySignupDefaults(ctx, &user, tx); err != nil {
return model.User{}, err
}
}
return user, nil return user, nil
} }
func (s *UserService) applySignupDefaults(ctx context.Context, user *model.User, tx *gorm.DB) error {
config := s.appConfigService.GetDbConfig()
// Apply default user groups
var groupIDs []string
if v := config.SignupDefaultUserGroupIDs.Value; v != "" && v != "[]" {
if err := json.Unmarshal([]byte(v), &groupIDs); err != nil {
return fmt.Errorf("invalid SignupDefaultUserGroupIDs JSON: %w", err)
}
if len(groupIDs) > 0 {
var groups []model.UserGroup
if err := tx.WithContext(ctx).Where("id IN ?", groupIDs).Find(&groups).Error; err != nil {
return fmt.Errorf("failed to find default user groups: %w", err)
}
if err := tx.WithContext(ctx).Model(user).Association("UserGroups").Replace(groups); err != nil {
return fmt.Errorf("failed to associate default user groups: %w", err)
}
}
}
// Apply default custom claims
var claims []dto.CustomClaimCreateDto
if v := config.SignupDefaultCustomClaims.Value; v != "" && v != "[]" {
if err := json.Unmarshal([]byte(v), &claims); err != nil {
return fmt.Errorf("invalid SignupDefaultCustomClaims JSON: %w", err)
}
if len(claims) > 0 {
if _, err := s.customClaimService.updateCustomClaimsInternal(ctx, UserID, user.ID, claims, tx); err != nil {
return fmt.Errorf("failed to apply default custom claims: %w", err)
}
}
}
return nil
}
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool) (model.User, error) { func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool) (model.User, error) {
tx := s.db.Begin() tx := s.db.Begin()
defer func() { defer func() {
@@ -348,13 +395,13 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
return user, nil return user, nil
} }
func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, userID string, expiration time.Time) error { func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, userID string, ttl time.Duration) error {
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsAdminEnabled.IsTrue() isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsAdminEnabled.IsTrue()
if isDisabled { if isDisabled {
return &common.OneTimeAccessDisabledError{} return &common.OneTimeAccessDisabledError{}
} }
return s.requestOneTimeAccessEmailInternal(ctx, userID, "", expiration) return s.requestOneTimeAccessEmailInternal(ctx, userID, "", ttl)
} }
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error { func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error {
@@ -374,11 +421,10 @@ func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context
} }
} }
expiration := time.Now().Add(15 * time.Minute) return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute)
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, expiration)
} }
func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, expiration time.Time) error { func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, ttl time.Duration) error {
tx := s.db.Begin() tx := s.db.Begin()
defer func() { defer func() {
tx.Rollback() tx.Rollback()
@@ -389,7 +435,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
return err return err
} }
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, expiration, tx) oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
if err != nil { if err != nil {
return err return err
} }
@@ -421,7 +467,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
Code: oneTimeAccessToken, Code: oneTimeAccessToken,
LoginLink: link, LoginLink: link,
LoginLinkWithCode: linkWithCode, LoginLinkWithCode: linkWithCode,
ExpirationString: utils.DurationToString(time.Until(expiration).Round(time.Second)), ExpirationString: utils.DurationToString(ttl),
}) })
if errInternal != nil { if errInternal != nil {
slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email)) slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email))
@@ -432,17 +478,18 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
return nil return nil
} }
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, expiresAt time.Time) (string, error) { func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error) {
return s.createOneTimeAccessTokenInternal(ctx, userID, expiresAt, s.db) return s.createOneTimeAccessTokenInternal(ctx, userID, ttl, s.db)
} }
func (s *UserService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, expiresAt time.Time, tx *gorm.DB) (string, error) { func (s *UserService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, ttl time.Duration, tx *gorm.DB) (string, error) {
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, expiresAt) oneTimeAccessToken, err := NewOneTimeAccessToken(userID, ttl)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := tx.WithContext(ctx).Create(oneTimeAccessToken).Error; err != nil { err = tx.WithContext(ctx).Create(oneTimeAccessToken).Error
if err != nil {
return "", err return "", err
} }
@@ -504,7 +551,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
// Fetch the groups based on userGroupIds // Fetch the groups based on userGroupIds
var groups []model.UserGroup var groups []model.UserGroup
if len(userGroupIds) > 0 { if len(userGroupIds) > 0 {
err = tx. err := tx.
WithContext(ctx). WithContext(ctx).
Where("id IN (?)", userGroupIds). Where("id IN (?)", userGroupIds).
Find(&groups). Find(&groups).
@@ -642,17 +689,14 @@ func (s *UserService) disableUserInternal(ctx context.Context, userID string, tx
Error Error
} }
func (s *UserService) CreateSignupToken(ctx context.Context, expiresAt time.Time, usageLimit int) (model.SignupToken, error) { func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int) (model.SignupToken, error) {
return s.createSignupTokenInternal(ctx, expiresAt, usageLimit, s.db) signupToken, err := NewSignupToken(ttl, usageLimit)
}
func (s *UserService) createSignupTokenInternal(ctx context.Context, expiresAt time.Time, usageLimit int, tx *gorm.DB) (model.SignupToken, error) {
signupToken, err := NewSignupToken(expiresAt, usageLimit)
if err != nil { if err != nil {
return model.SignupToken{}, err return model.SignupToken{}, err
} }
if err := tx.WithContext(ctx).Create(signupToken).Error; err != nil { err = s.db.WithContext(ctx).Create(signupToken).Error
if err != nil {
return model.SignupToken{}, err return model.SignupToken{}, err
} }
@@ -746,10 +790,10 @@ func (s *UserService) DeleteSignupToken(ctx context.Context, tokenID string) err
return s.db.WithContext(ctx).Delete(&model.SignupToken{}, "id = ?", tokenID).Error return s.db.WithContext(ctx).Delete(&model.SignupToken{}, "id = ?", tokenID).Error
} }
func NewOneTimeAccessToken(userID string, expiresAt time.Time) (*model.OneTimeAccessToken, error) { func NewOneTimeAccessToken(userID string, ttl time.Duration) (*model.OneTimeAccessToken, error) {
// If expires at is less than 15 minutes, use a 6-character token instead of 16 // If expires at is less than 15 minutes, use a 6-character token instead of 16
tokenLength := 16 tokenLength := 16
if time.Until(expiresAt) <= 15*time.Minute { if ttl <= 15*time.Minute {
tokenLength = 6 tokenLength = 6
} }
@@ -758,25 +802,27 @@ func NewOneTimeAccessToken(userID string, expiresAt time.Time) (*model.OneTimeAc
return nil, err return nil, err
} }
now := time.Now().Round(time.Second)
o := &model.OneTimeAccessToken{ o := &model.OneTimeAccessToken{
UserID: userID, UserID: userID,
ExpiresAt: datatype.DateTime(expiresAt), ExpiresAt: datatype.DateTime(now.Add(ttl)),
Token: randomString, Token: randomString,
} }
return o, nil return o, nil
} }
func NewSignupToken(expiresAt time.Time, usageLimit int) (*model.SignupToken, error) { func NewSignupToken(ttl time.Duration, usageLimit int) (*model.SignupToken, error) {
// Generate a random token // Generate a random token
randomString, err := utils.GenerateRandomAlphanumericString(16) randomString, err := utils.GenerateRandomAlphanumericString(16)
if err != nil { if err != nil {
return nil, err return nil, err
} }
now := time.Now().Round(time.Second)
token := &model.SignupToken{ token := &model.SignupToken{
Token: randomString, Token: randomString,
ExpiresAt: datatype.DateTime(expiresAt), ExpiresAt: datatype.DateTime(now.Add(ttl)),
UsageLimit: usageLimit, UsageLimit: usageLimit,
UsageCount: 0, UsageCount: 0,
} }

View File

@@ -221,13 +221,15 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
tx.Rollback() tx.Rollback()
}() }()
// Load & delete the session row
var storedSession model.WebauthnSession var storedSession model.WebauthnSession
err := tx. err := tx.
WithContext(ctx). WithContext(ctx).
First(&storedSession, "id = ?", sessionID). Clauses(clause.Returning{}).
Delete(&storedSession, "id = ?", sessionID).
Error Error
if err != nil { if err != nil {
return model.User{}, "", err return model.User{}, "", fmt.Errorf("failed to load WebAuthn session: %w", err)
} }
session := webauthn.SessionData{ session := webauthn.SessionData{
@@ -334,3 +336,136 @@ func (s *WebAuthnService) UpdateCredential(ctx context.Context, userID, credenti
func (s *WebAuthnService) updateWebAuthnConfig() { func (s *WebAuthnService) updateWebAuthnConfig() {
s.webAuthn.Config.RPDisplayName = s.appConfigService.GetDbConfig().AppName.Value s.webAuthn.Config.RPDisplayName = s.appConfigService.GetDbConfig().AppName.Value
} }
func (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(ctx context.Context, accessToken string) (string, error) {
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
token, err := s.jwtService.VerifyAccessToken(accessToken)
if err != nil {
return "", fmt.Errorf("invalid access token: %w", err)
}
userID, ok := token.Subject()
if !ok {
return "", fmt.Errorf("access token does not contain user ID")
}
// Check if token is issued less than a minute ago
tokenExpiration, ok := token.IssuedAt()
if !ok || time.Since(tokenExpiration) > time.Minute {
return "", &common.ReauthenticationRequiredError{}
}
var user model.User
err = tx.
WithContext(ctx).
First(&user, "id = ?", userID).
Error
if err != nil {
return "", fmt.Errorf("failed to load user: %w", err)
}
reauthToken, err := s.createReauthenticationToken(ctx, tx, user.ID)
if err != nil {
return "", err
}
err = tx.Commit().Error
if err != nil {
return "", err
}
return reauthToken, nil
}
func (s *WebAuthnService) CreateReauthenticationTokenWithWebauthn(ctx context.Context, sessionID string, credentialAssertionData *protocol.ParsedCredentialAssertionData) (string, error) {
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
// Retrieve and delete the session
var storedSession model.WebauthnSession
err := tx.
WithContext(ctx).
Clauses(clause.Returning{}).
Delete(&storedSession, "id = ? AND expires_at > ?", sessionID, datatype.DateTime(time.Now())).
Error
if err != nil {
return "", fmt.Errorf("failed to load WebAuthn session: %w", err)
}
session := webauthn.SessionData{
Challenge: storedSession.Challenge,
Expires: storedSession.ExpiresAt.ToTime(),
}
// Validate the credential assertion
var user *model.User
_, err = s.webAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (webauthn.User, error) {
innerErr := tx.
WithContext(ctx).
Preload("Credentials").
First(&user, "id = ?", string(userHandle)).
Error
if innerErr != nil {
return nil, innerErr
}
return user, nil
}, session, credentialAssertionData)
if err != nil || user == nil {
return "", err
}
// Create reauthentication token
token, err := s.createReauthenticationToken(ctx, tx, user.ID)
if err != nil {
return "", err
}
err = tx.Commit().Error
if err != nil {
return "", err
}
return token, nil
}
func (s *WebAuthnService) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) error {
hashedToken := utils.CreateSha256Hash(token)
result := tx.WithContext(ctx).
Clauses(clause.Returning{}).
Delete(&model.ReauthenticationToken{}, "token = ? AND user_id = ? AND expires_at > ?", hashedToken, userID, datatype.DateTime(time.Now()))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return &common.ReauthenticationRequiredError{}
}
return nil
}
func (s *WebAuthnService) createReauthenticationToken(ctx context.Context, tx *gorm.DB, userID string) (string, error) {
token, err := utils.GenerateRandomAlphanumericString(32)
if err != nil {
return "", err
}
reauthToken := model.ReauthenticationToken{
Token: utils.CreateSha256Hash(token),
ExpiresAt: datatype.DateTime(time.Now().Add(3 * time.Minute)),
UserID: userID,
}
err = tx.WithContext(ctx).Create(&reauthToken).Error
if err != nil {
return "", err
}
return token, nil
}

View File

@@ -0,0 +1,42 @@
package utils
import (
"encoding/json"
"errors"
"time"
)
// JSONDuration is a type that allows marshalling/unmarshalling a Duration
type JSONDuration struct {
time.Duration
}
func (d JSONDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d *JSONDuration) UnmarshalJSON(b []byte) error {
var v any
err := json.Unmarshal(b, &v)
if err != nil {
return err
}
switch value := v.(type) {
case float64:
// If the value is a number, interpret it as a number of seconds
d.Duration = time.Duration(value) * time.Second
return nil
case string:
if v == "" {
return nil
}
var err error
d.Duration, err = time.ParseDuration(value)
if err != nil {
return err
}
return nil
default:
return errors.New("invalid duration")
}
}

View File

@@ -0,0 +1,64 @@
package utils
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONDuration_MarshalJSON(t *testing.T) {
tests := []struct {
duration time.Duration
want string
}{
{time.Minute + 30*time.Second, "1m30s"},
{0, "0s"},
}
for _, tc := range tests {
d := JSONDuration{Duration: tc.duration}
b, err := json.Marshal(d)
require.NoError(t, err)
assert.Equal(t, `"`+tc.want+`"`, string(b))
}
}
func TestJSONDuration_UnmarshalJSON_String(t *testing.T) {
var d JSONDuration
err := json.Unmarshal([]byte(`"2h15m5s"`), &d)
require.NoError(t, err)
want := 2*time.Hour + 15*time.Minute + 5*time.Second
assert.Equal(t, want, d.Duration)
}
func TestJSONDuration_UnmarshalJSON_NumberSeconds(t *testing.T) {
tests := []struct {
json string
want time.Duration
}{
{"0", 0},
{"1", 1 * time.Second},
{"2.25", 2 * time.Second}, // Milliseconds are truncated
}
for _, tc := range tests {
var d JSONDuration
err := json.Unmarshal([]byte(tc.json), &d)
require.NoError(t, err, "input: %s", tc.json)
assert.Equal(t, tc.want, d.Duration, "input: %s", tc.json)
}
}
func TestJSONDuration_UnmarshalJSON_Invalid(t *testing.T) {
cases := [][]byte{
[]byte(`true`),
[]byte(`{}`),
[]byte(`"not-a-duration"`),
}
for _, b := range cases {
var d JSONDuration
err := json.Unmarshal(b, &d)
require.Error(t, err, "input: %s", string(b))
}
}

View File

@@ -3,6 +3,7 @@ package utils
import ( import (
"reflect" "reflect"
"strconv" "strconv"
"strings"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -35,9 +36,7 @@ func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gor
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn) sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable")) isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
if sort.Direction == "" || (sort.Direction != "asc" && sort.Direction != "desc") { sort.Direction = NormalizeSortDirection(sort.Direction)
sort.Direction = "asc"
}
if sortFieldFound && isSortable { if sortFieldFound && isSortable {
columnName := CamelCaseToSnakeCase(sort.Column) columnName := CamelCaseToSnakeCase(sort.Column)
@@ -85,3 +84,16 @@ func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (Pagin
ItemsPerPage: pageSize, ItemsPerPage: pageSize,
}, nil }, 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"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
ALTER TABLE oidc_clients DROP COLUMN requires_reauthentication;
DROP TABLE IF EXISTS reauthentication_tokens;

View File

@@ -0,0 +1,11 @@
ALTER TABLE oidc_clients ADD COLUMN requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE;
CREATE TABLE reauthentication_tokens (
id TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
);
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);

View File

@@ -0,0 +1,8 @@
ALTER TABLE public.audit_logs
DROP CONSTRAINT IF EXISTS audit_logs_user_id_fkey,
ADD CONSTRAINT audit_logs_user_id_fkey
FOREIGN KEY (user_id) REFERENCES public.users (id) ON DELETE CASCADE;
ALTER TABLE public.oidc_authorization_codes
ADD CONSTRAINT oidc_authorization_codes_client_fk
FOREIGN KEY (client_id) REFERENCES public.oidc_clients (id) ON DELETE CASCADE;

View File

@@ -0,0 +1,3 @@
ALTER TABLE oidc_clients DROP COLUMN requires_reauthentication;
DROP INDEX IF EXISTS idx_reauthentication_tokens_token;
DROP TABLE IF EXISTS reauthentication_tokens;

View File

@@ -0,0 +1,11 @@
ALTER TABLE oidc_clients ADD COLUMN requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE;
CREATE TABLE reauthentication_tokens (
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
);
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);

View File

@@ -0,0 +1,173 @@
---------------------------
-- Delete all orphaned rows
---------------------------
UPDATE oidc_clients
SET created_by_id = NULL
WHERE created_by_id IS NOT NULL
AND created_by_id NOT IN (SELECT id FROM users);
DELETE FROM oidc_authorization_codes WHERE user_id NOT IN (SELECT id FROM users);
DELETE FROM one_time_access_tokens WHERE user_id NOT IN (SELECT id FROM users);
DELETE FROM webauthn_credentials WHERE user_id NOT IN (SELECT id FROM users);
DELETE FROM audit_logs WHERE user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users);
DELETE FROM api_keys WHERE user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users);
DELETE FROM oidc_refresh_tokens WHERE user_id NOT IN (SELECT id FROM users) OR client_id NOT IN (SELECT id FROM oidc_clients);
DELETE FROM oidc_device_codes WHERE (user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users)) OR client_id NOT IN (SELECT id FROM oidc_clients);
DELETE FROM user_authorized_oidc_clients WHERE user_id NOT IN (SELECT id FROM users) OR client_id NOT IN (SELECT id FROM oidc_clients);
DELETE FROM user_groups_users WHERE user_id NOT IN (SELECT id FROM users) OR user_group_id NOT IN (SELECT id FROM user_groups);
DELETE FROM custom_claims WHERE (user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users)) OR (user_group_id IS NOT NULL AND user_group_id NOT IN (SELECT id FROM user_groups));
DELETE FROM oidc_clients_allowed_user_groups WHERE oidc_client_id NOT IN (SELECT id FROM oidc_clients) OR user_group_id NOT IN (SELECT id FROM user_groups);
DELETE FROM reauthentication_tokens WHERE user_id NOT IN (SELECT id FROM users);
---------------------------
-- Add missing foreign keys and edit cascade behavior where necessary
---------------------------
-- reauthentication_tokens: add missing FK user_id → users
CREATE TABLE reauthentication_tokens_new
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
);
INSERT INTO reauthentication_tokens_new (id, created_at, token, expires_at, user_id)
SELECT id, created_at, token, expires_at, user_id
FROM reauthentication_tokens;
DROP TABLE reauthentication_tokens;
ALTER TABLE reauthentication_tokens_new RENAME TO reauthentication_tokens;
CREATE INDEX idx_reauthentication_tokens_token
ON reauthentication_tokens (token);
-- oidc_authorization_codes: add FK client_id, user_id → CASCADE
CREATE TABLE oidc_authorization_codes_new
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
code TEXT NOT NULL UNIQUE,
scope TEXT NOT NULL,
nonce TEXT,
expires_at DATETIME NOT NULL,
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
code_challenge TEXT,
code_challenge_method_sha256 NUMERIC
);
INSERT INTO oidc_authorization_codes_new
(id, created_at, code, scope, nonce, expires_at, user_id, client_id, code_challenge, code_challenge_method_sha256)
SELECT id, created_at, code, scope, nonce, expires_at, user_id, client_id, code_challenge, code_challenge_method_sha256
FROM oidc_authorization_codes;
DROP TABLE oidc_authorization_codes;
ALTER TABLE oidc_authorization_codes_new RENAME TO oidc_authorization_codes;
-- user_authorized_oidc_clients: add FK user_id, cascade client_id
CREATE TABLE user_authorized_oidc_clients_new
(
scope TEXT,
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
last_used_at DATETIME NOT NULL,
PRIMARY KEY (user_id, client_id)
);
INSERT INTO user_authorized_oidc_clients_new (scope, user_id, client_id, last_used_at)
SELECT scope, user_id, client_id, last_used_at
FROM user_authorized_oidc_clients;
DROP TABLE user_authorized_oidc_clients;
ALTER TABLE user_authorized_oidc_clients_new RENAME TO user_authorized_oidc_clients;
-- audit_logs: user_id → CASCADE
CREATE TABLE audit_logs_new
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
event TEXT NOT NULL,
ip_address TEXT,
user_agent TEXT NOT NULL,
data BLOB NOT NULL,
user_id TEXT REFERENCES users ON DELETE CASCADE,
country TEXT,
city TEXT
);
INSERT INTO audit_logs_new
(id, created_at, event, ip_address, user_agent, data, user_id, country, city)
SELECT id, created_at, event, ip_address, user_agent, data, user_id, country, city
FROM audit_logs;
DROP TABLE audit_logs;
ALTER TABLE audit_logs_new RENAME TO audit_logs;
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
CREATE INDEX idx_audit_logs_country ON audit_logs (country);
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);
CREATE INDEX idx_audit_logs_event ON audit_logs (event);
CREATE INDEX idx_audit_logs_user_agent ON audit_logs (user_agent);
CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
-- oidc_clients: created_by_id → SET NULL
CREATE TABLE oidc_clients_new
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
name TEXT,
secret TEXT,
callback_urls BLOB,
image_type TEXT,
created_by_id TEXT REFERENCES users ON DELETE SET NULL,
is_public BOOLEAN DEFAULT FALSE,
pkce_enabled BOOLEAN DEFAULT FALSE,
logout_callback_urls BLOB,
credentials TEXT,
launch_url TEXT,
requires_reauthentication BOOLEAN DEFAULT FALSE NOT NULL
);
INSERT INTO oidc_clients_new
(id, created_at, name, secret, callback_urls, image_type, created_by_id,
is_public, pkce_enabled, logout_callback_urls, credentials, launch_url, requires_reauthentication)
SELECT id, created_at, name, secret, callback_urls, image_type, created_by_id,
is_public, pkce_enabled, logout_callback_urls, credentials, launch_url, requires_reauthentication
FROM oidc_clients;
DROP TABLE oidc_clients;
ALTER TABLE oidc_clients_new RENAME TO oidc_clients;
-- one_time_access_tokens: user_id → CASCADE
CREATE TABLE one_time_access_tokens_new
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
);
INSERT INTO one_time_access_tokens_new
(id, created_at, token, expires_at, user_id)
SELECT id, created_at, token, expires_at, user_id
FROM one_time_access_tokens;
DROP TABLE one_time_access_tokens;
ALTER TABLE one_time_access_tokens_new RENAME TO one_time_access_tokens;
-- webauthn_credentials: user_id → CASCADE
CREATE TABLE webauthn_credentials_new
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
name TEXT NOT NULL,
credential_id TEXT NOT NULL UNIQUE,
public_key BLOB NOT NULL,
attestation_type TEXT NOT NULL,
transport BLOB NOT NULL,
user_id TEXT REFERENCES users ON DELETE CASCADE,
backup_eligible BOOLEAN DEFAULT FALSE NOT NULL,
backup_state BOOLEAN DEFAULT FALSE NOT NULL
);
INSERT INTO webauthn_credentials_new
(id, created_at, name, credential_id, public_key, attestation_type,
transport, user_id, backup_eligible, backup_state)
SELECT id, created_at, name, credential_id, public_key, attestation_type,
transport, user_id, backup_eligible, backup_state
FROM webauthn_credentials;
DROP TABLE webauthn_credentials;
ALTER TABLE webauthn_credentials_new RENAME TO webauthn_credentials;

View File

@@ -429,5 +429,6 @@
"client_name_description": "Název klienta, který se zobrazuje v uživatelském rozhraní Pocket ID.", "client_name_description": "Název klienta, který se zobrazuje v uživatelském rozhraní Pocket ID.",
"revoke_access": "Zrušit přístup", "revoke_access": "Zrušit přístup",
"revoke_access_description": "Zrušit přístup k <b>{clientName}</b>. <b>{clientName}</b> už nebude mít přístup k informacím o vašem účtu.", "revoke_access_description": "Zrušit přístup k <b>{clientName}</b>. <b>{clientName}</b> už nebude mít přístup k informacím o vašem účtu.",
"revoke_access_successful": "Přístup k {clientName} byl úspěšně zrušen." "revoke_access_successful": "Přístup k {clientName} byl úspěšně zrušen.",
"last_signed_in_ago": "Naposledy přihlášen {time} před"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.", "client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.",
"revoke_access": "Tilbagekald adgang", "revoke_access": "Tilbagekald adgang",
"revoke_access_description": "Tilbagekald adgang til <b>{clientName}</b>. <b>{clientName}</b> vil ikke længere kunne få adgang til dine kontooplysninger.", "revoke_access_description": "Tilbagekald adgang til <b>{clientName}</b>. <b>{clientName}</b> vil ikke længere kunne få adgang til dine kontooplysninger.",
"revoke_access_successful": "Adgangen til {clientName} er blevet ophævet." "revoke_access_successful": "Adgangen til {clientName} er blevet ophævet.",
"last_signed_in_ago": "Sidst logget ind {time} siden"
} }

View File

@@ -276,6 +276,8 @@
"public_clients_description": "Öffentliche Clients haben kein Client-Geheimnis und verwenden stattdessen PKCE. Aktiviere dies, wenn dein Client eine SPA oder mobile App ist.", "public_clients_description": "Öffentliche Clients haben kein Client-Geheimnis und verwenden stattdessen PKCE. Aktiviere dies, wenn dein Client eine SPA oder mobile App ist.",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Der Public Key Code Exchange (öffentlicher Schlüsselaustausch) ist eine Sicherheitsfunktion, um CSRF Angriffe und Angriffe zum Abfangen von Autorisierungscodes zu verhindern.", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Der Public Key Code Exchange (öffentlicher Schlüsselaustausch) ist eine Sicherheitsfunktion, um CSRF Angriffe und Angriffe zum Abfangen von Autorisierungscodes zu verhindern.",
"requires_reauthentication": "Erfordert erneute Authentifizierung",
"requires_users_to_authenticate_again_on_each_authorization": "Erfordert eine neue Authentifizierung bei jeder Autorisierung, auch wenn der Benutzer bereits angemeldet ist",
"name_logo": "{name} Logo", "name_logo": "{name} Logo",
"change_logo": "Logo ändern", "change_logo": "Logo ändern",
"upload_logo": "Logo hochladen", "upload_logo": "Logo hochladen",
@@ -429,5 +431,6 @@
"client_name_description": "Der Name des Clients, der in der Pocket ID-Benutzeroberfläche angezeigt wird.", "client_name_description": "Der Name des Clients, der in der Pocket ID-Benutzeroberfläche angezeigt wird.",
"revoke_access": "Zugriff widerrufen", "revoke_access": "Zugriff widerrufen",
"revoke_access_description": "Zugriff widerrufen <b>{clientName}</b>. <b>{clientName}</b> kann nicht mehr auf deine Kontoinfos zugreifen.", "revoke_access_description": "Zugriff widerrufen <b>{clientName}</b>. <b>{clientName}</b> kann nicht mehr auf deine Kontoinfos zugreifen.",
"revoke_access_successful": "Der Zugriff auf „ {clientName} “ wurde erfolgreich gesperrt." "revoke_access_successful": "Der Zugriff auf „ {clientName} “ wurde erfolgreich gesperrt.",
"last_signed_in_ago": "Zuletzt angemeldet vor {time} Stunden"
} }

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Click to copy", "click_to_copy": "Click to copy",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Something went wrong",
"go_back_to_home": "Go back to home", "go_back_to_home": "Go back to home",
"dont_have_access_to_your_passkey": "Don't have access to your passkey?", "alternative_sign_in_methods": "Alternative Sign In Methods",
"login_background": "Login background", "login_background": "Login background",
"logo": "Logo", "logo": "Logo",
"login_code": "Login Code", "login_code": "Login Code",
@@ -276,6 +276,8 @@
"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.", "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", "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.", "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 Re-Authentication",
"requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in",
"name_logo": "{name} logo", "name_logo": "{name} logo",
"change_logo": "Change Logo", "change_logo": "Change Logo",
"upload_logo": "Upload Logo", "upload_logo": "Upload Logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Number of times the signup token can be used.", "number_of_times_token_can_be_used": "Number of times the signup token can be used.",
"expires": "Expires", "expires": "Expires",
"signup": "Sign Up", "signup": "Sign Up",
"user_creation": "User Creation",
"configure_user_creation": "Manage user creation settings, including signup methods and default permissions for new users.",
"user_creation_groups_description": "Assign these groups automatically to new users upon signup.",
"user_creation_claims_description": "Assign these custom claims automatically to new users upon signup.",
"user_creation_updated_successfully": "User creation settings updated successfully.",
"signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.",
"signup_requires_valid_token": "A valid signup token is required to create an account", "signup_requires_valid_token": "A valid signup token is required to create an account",
"validating_signup_token": "Validating signup token", "validating_signup_token": "Validating signup token",
"go_to_login": "Go to login", "go_to_login": "Go to login",
@@ -396,7 +404,7 @@
"skip_for_now": "Skip for now", "skip_for_now": "Skip for now",
"account_created": "Account Created", "account_created": "Account Created",
"enable_user_signups": "Enable User Signups", "enable_user_signups": "Enable User Signups",
"enable_user_signups_description": "Whether the User Signup functionality should be enabled.", "enable_user_signups_description": "Decide how users can sign up for new accounts in Pocket ID.",
"user_signups_are_disabled": "User signups are currently disabled", "user_signups_are_disabled": "User signups are currently disabled",
"create_signup_token": "Create Signup Token", "create_signup_token": "Create Signup Token",
"view_active_signup_tokens": "View Active Signup Tokens", "view_active_signup_tokens": "View Active Signup Tokens",
@@ -412,7 +420,6 @@
"loading": "Loading", "loading": "Loading",
"delete_signup_token": "Delete Signup Token", "delete_signup_token": "Delete Signup Token",
"are_you_sure_you_want_to_delete_this_signup_token": "Are you sure you want to delete this signup token? This action cannot be undone.", "are_you_sure_you_want_to_delete_this_signup_token": "Are you sure you want to delete this signup token? This action cannot be undone.",
"signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.",
"signup_with_token": "Signup with token", "signup_with_token": "Signup with token",
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.", "signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
"signup_open": "Open Signup", "signup_open": "Open Signup",
@@ -429,5 +436,9 @@
"client_name_description": "The name of the client that shows in the Pocket ID UI.", "client_name_description": "The name of the client that shows in the Pocket ID UI.",
"revoke_access": "Revoke Access", "revoke_access": "Revoke Access",
"revoke_access_description": "Revoke access to <b>{clientName}</b>. <b>{clientName}</b> will no longer be able to access your account information.", "revoke_access_description": "Revoke access to <b>{clientName}</b>. <b>{clientName}</b> will no longer be able to access your account information.",
"revoke_access_successful": "The access to {clientName} has been successfully revoked." "revoke_access_successful": "The access to {clientName} has been successfully revoked.",
"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"
} }

View File

@@ -385,6 +385,12 @@
"number_of_times_token_can_be_used": "Número de veces que se puede utilizar el token de registro.", "number_of_times_token_can_be_used": "Número de veces que se puede utilizar el token de registro.",
"expires": "Caduca", "expires": "Caduca",
"signup": "Regístrate", "signup": "Regístrate",
"user_creation": "Registro",
"configure_user_creation": "Gestiona la configuración de registro de usuarios, incluyendo los métodos de registro y los permisos por defecto para nuevos usuarios.",
"user_creation_groups_description": "Asigna estos grupos automáticamente a los nuevos usuarios al registrarse.",
"user_creation_claims_description": "Asigna estas reclamaciones personalizadas automáticamente a los nuevos usuarios al registrarse.",
"user_creation_updated_successfully": "Configuración de registro actualizada correctamente.",
"signup_disabled_description": "El registro de usuarios está completamente desactivado. Solo los administradores pueden crear nuevas cuentas de usuario.",
"signup_requires_valid_token": "Se requiere un token de registro válido para crear una cuenta.", "signup_requires_valid_token": "Se requiere un token de registro válido para crear una cuenta.",
"validating_signup_token": "Validación del token de registro", "validating_signup_token": "Validación del token de registro",
"go_to_login": "Ir al inicio de sesión", "go_to_login": "Ir al inicio de sesión",
@@ -412,7 +418,6 @@
"loading": "Cargando", "loading": "Cargando",
"delete_signup_token": "Eliminar token de registro", "delete_signup_token": "Eliminar token de registro",
"are_you_sure_you_want_to_delete_this_signup_token": "¿Estás seguro de que deseas eliminar este token de registro? Esta acción no se puede deshacer.", "are_you_sure_you_want_to_delete_this_signup_token": "¿Estás seguro de que deseas eliminar este token de registro? Esta acción no se puede deshacer.",
"signup_disabled_description": "El registro de usuarios está completamente desactivado. Solo los administradores pueden crear nuevas cuentas de usuario.",
"signup_with_token": "Regístrate con token", "signup_with_token": "Regístrate con token",
"signup_with_token_description": "Los usuarios solo pueden registrarse utilizando un token de registro válido creado por un administrador.", "signup_with_token_description": "Los usuarios solo pueden registrarse utilizando un token de registro válido creado por un administrador.",
"signup_open": "Inscripción abierta", "signup_open": "Inscripción abierta",
@@ -429,5 +434,6 @@
"client_name_description": "El nombre del cliente que aparece en la interfaz de usuario de Pocket ID.", "client_name_description": "El nombre del cliente que aparece en la interfaz de usuario de Pocket ID.",
"revoke_access": "Revocar acceso", "revoke_access": "Revocar acceso",
"revoke_access_description": "Revocar el acceso a <b>{clientName}</b>. <b>{clientName}</b> ya no podrás acceder a la información de tu cuenta.", "revoke_access_description": "Revocar el acceso a <b>{clientName}</b>. <b>{clientName}</b> ya no podrás acceder a la información de tu cuenta.",
"revoke_access_successful": "El acceso a {clientName} ha sido revocado correctamente." "revoke_access_successful": "El acceso a {clientName} ha sido revocado correctamente.",
"last_signed_in_ago": "Último inicio de sesión en {time} hace"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.", "client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
"revoke_access": "Supprimer l'accès", "revoke_access": "Supprimer l'accès",
"revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de ton compte.", "revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de ton compte.",
"revoke_access_successful": "L'accès à {clientName} a été supprimé." "revoke_access_successful": "L'accès à {clientName} a été supprimé.",
"last_signed_in_ago": "Dernière connexion il y a {time} il y a"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "Il nome del cliente che appare nell'interfaccia utente Pocket ID.", "client_name_description": "Il nome del cliente che appare nell'interfaccia utente Pocket ID.",
"revoke_access": "Revoca accesso", "revoke_access": "Revoca accesso",
"revoke_access_description": "Revoca l'accesso a <b>{clientName}</b>. <b>{clientName}</b> non potrà più accedere alle informazioni del tuo account.", "revoke_access_description": "Revoca l'accesso a <b>{clientName}</b>. <b>{clientName}</b> non potrà più accedere alle informazioni del tuo account.",
"revoke_access_successful": "L'accesso a {clientName} è stato revocato con successo." "revoke_access_successful": "L'accesso a {clientName} è stato revocato con successo.",
"last_signed_in_ago": "Ultimo accesso {time} fa"
} }

434
frontend/messages/ko.json Normal file
View File

@@ -0,0 +1,434 @@
{
"$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": "항목 없음",
"search": "검색...",
"expand_card": "카드 확장",
"copied": "복사됨",
"click_to_copy": "클릭하여 복사",
"something_went_wrong": "문제가 발생했습니다",
"go_back_to_home": "홈으로 돌아가기",
"dont_have_access_to_your_passkey": "패스키에 접근할 수 없나요?",
"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": "인증기가 발견 가능한 자격 증명을 지원하지 않습니다",
"authenticator_does_not_support_resident_keys": "인증기가 레지던트 키를 지원하지 않습니다",
"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": "{appName}에서 계정 <b>{username}</b>을 로그아웃하시겠습니까?",
"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": "로그인 코드를 입력하여 로그인하세요.",
"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": "잘못된 리다이렉트 URL",
"audit_log": "감사 로그",
"users": "사용자",
"user_groups": "사용자 그룹",
"oidc_clients": "OIDC 클라이언트",
"api_keys": "API 키",
"application_configuration": "애플리케이션 설정",
"settings": "설정",
"update_pocket_id": "Pocket ID 업데이트",
"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": "패스키가 하나만 구성되었습니다",
"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": "사용자 이름은 영어 소문자, 숫자, 밑줄, 점, 하이픈, '@' 기호만 포함할 수 있습니다",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "다음 코드를 사용하여 로그인하세요. 이 코드는 15분 후에 만료됩니다.",
"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": "프로그램 접근을 위해 새로운 API 키를 추가합니다.",
"add_api_key": "API 키 추가",
"manage_api_keys": "API 키 관리",
"api_key_created": "API 키 생성됨",
"for_security_reasons_this_key_will_only_be_shown_once": "보안상의 이유로 이 키는 한 번만 표시됩니다. 안전하게 보관해 주세요.",
"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}\"를 정말로 취소하시겠습니까? 이 키를 사용하는 모든 통합이 작동하지 않습니다.",
"last_used": "마지막 사용",
"actions": "동작",
"images_updated_successfully": "이미지가 성공적으로 업데이트되었습니다",
"general": "일반",
"configure_smtp_to_send_emails": "새로운 기기나 위치에서 로그인 감지 시 이메일 알림을 활성화합니다.",
"ldap": "LDAP",
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP 서버에서 사용자와 그룹을 동기화하기 위해 LDAP을 구성합니다.",
"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": "테스트 이메일을 전송하기 전에 변경 내용을 저장해야 합니다. 지금 저장하시겠습니까?",
"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": "인증서 검증 건너뛰기",
"this_can_be_useful_for_selfsigned_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": "이메일 인증됨",
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "OIDC 클라이언트에게 사용자의 이메일이 인증된 것으로 표시합니다.",
"ldap_configuration_updated_successfully": "LDAP 구성이 성공적으로 업데이트되었습니다",
"ldap_disabled_successfully": "LDAP가 성공적으로 비활성화되었습니다",
"ldap_sync_finished": "LDAP 동기화 완료",
"client_configuration": "클라이언트 구성",
"ldap_url": "LDAP URL",
"ldap_bind_dn": "LDAP 바인드 DN",
"ldap_bind_password": "LDAP 바인드 비밀번호",
"ldap_base_dn": "LDAP 베이스 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": "사용자 고유 식별자 속성",
"the_value_of_this_attribute_should_never_change": "이 속성의 값은 절대 변경되면 안 됩니다.",
"username_attribute": "사용자 이름 속성",
"user_mail_attribute": "사용자 이메일 속성",
"user_first_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": "그룹의 멤버를 질의할 때 사용할 속성.",
"group_unique_identifier_attribute": "그룹 고유 식별자 속성",
"group_name_attribute": "그룹 멤버 속성",
"admin_group_name": "관리자 그룹 이름",
"members_of_this_group_will_have_admin_privileges_in_pocketid": "이 그룹의 멤버들은 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": "소스",
"admin": "관리자",
"user": "사용자",
"local": "로컬",
"toggle_menu": "메뉴 표시 전환",
"edit": "편집",
"user_groups_updated_successfully": "사용자 그룹이 성공적으로 업데이트되었습니다",
"user_updated_successfully": "사용자가 성공적으로 업데이트되었습니다",
"custom_claims_updated_successfully": "사용자 정의 클레임이 성공적으로 업데이트되었습니다",
"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": "별칭",
"name_that_will_be_displayed_in_the_ui": "UI에 표시되는 이름",
"name_that_will_be_in_the_groups_claim": "\"groups\" 클레임에 표시되는 이름",
"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 클라이언트가 성공적으로 생성되었습니다",
"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": "콜백 URL",
"logout_callback_urls": "로그아웃 콜백 URL",
"public_client": "공개 클라이언트",
"public_clients_description": "공개 클라이언트는 클라이언트 시크릿이 없습니다. 이들은 시크릿을 안전하게 보관할 수 없는 모바일, 웹, 네이티브 애플리케이션을 위해 설계되었습니다.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "공개 키 코드 교환은 CSRF 및 승인 코드 가로채기 공격을 방지하기 위한 보안 기능입니다.",
"name_logo": "{name} 로고",
"change_logo": "로고 변경",
"upload_logo": "로고 업로드",
"remove_logo": "로고 삭제",
"are_you_sure_you_want_to_delete_this_oidc_client": "이 OIDC 클라이언트를 삭제하시겠습니까?",
"oidc_client_deleted_successfully": "OIDC 클라이언트가 성공적으로 삭제되었습니다",
"authorization_url": "승인 URL",
"oidc_discovery_url": "OIDC 디스커버리 URL",
"token_url": "토큰 URL",
"userinfo_url": "사용자 정보 URL",
"logout_url": "로그아웃 URL",
"certificate_url": "인증서 URL",
"enabled": "활성화됨",
"disabled": "비활성화됨",
"oidc_client_updated_successfully": "OIDC 클라이언트가 성공적으로 업데이트되었습니다",
"create_new_client_secret": "새로운 클라이언트 시크릿 생성",
"are_you_sure_you_want_to_create_a_new_client_secret": "새로운 클라이언트 시크릿 생성하시겠습니까? 기존 클라이언트 시크릿은 무효화됩니다.",
"generate": "생성",
"new_client_secret_created_successfully": "새로운 클라이언트 시크릿이 성공적으로 생성되었습니다",
"allowed_user_groups_updated_successfully": "허용된 사용자 그룹이 성공적으로 업데이트되었습니다",
"oidc_client_name": "OIDC 클라이언트 {name}",
"client_id": "클라이언트 ID",
"client_secret": "클라이언트 시크릿",
"show_more_details": "상세 정보 보기",
"allowed_user_groups": "허용된 사용자 그룹",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "이 클라이언트에 사용자 그룹을 추가하여 해당 그룹의 사용자의 접근를 제한합니다. 사용자 그룹을 선택하지 않으면 모든 사용자가 이 클라이언트에 접근할 수 있습니다.",
"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": "개인",
"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": "클라이언트 승인",
"new_client_authorization": "새로운 클라이언트 승인",
"disable_animations": "애니메이션 비활성화",
"turn_off_ui_animations": "UI 전체의 애니메이션을 비활성화합니다.",
"user_disabled": "계정 비활성화",
"disabled_users_cannot_log_in_or_use_services": "비활성화된 사용자는 로그인하거나 서비스를 사용할 수 없습니다.",
"user_disabled_successfully": "사용자가 성공적으로 비활성화되었습니다.",
"user_enabled_successfully": "사용자가 성공적으로 활성화되었습니다.",
"status": "상태",
"disable_firstname_lastname": "{firstName} {lastName} 비활성화",
"are_you_sure_you_want_to_disable_this_user": "이 사용자를 비활성화하시겠습니까? 이 사용자는 로그인하거나 어떤 서비스에도 접근할 수 없게 됩니다.",
"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": "승인",
"federated_client_credentials": "연동 클라이언트 자격 증명",
"federated_client_credentials_description": "연동 클라이언트 자격 증명을 이용하여, OIDC 클라이언트를 제3자 인증 기관에서 발급한 JWT 토큰을 이용해 인증할 수 있습니다.",
"add_federated_client_credential": "연동 클라이언트 자격 증명 추가",
"add_another_federated_client_credential": "다른 연동 클라이언트 자격 증명 추가",
"oidc_allowed_group_count": "허용된 그룹 수",
"unrestricted": "제한 없음",
"show_advanced_options": "고급 옵션 표시",
"hide_advanced_options": "고급 옵션 숨기기",
"oidc_data_preview": "OIDC 데이터 미리보기",
"preview_the_oidc_data_that_would_be_sent_for_different_users": "여러 사용자를 위해 전송될 OIDC 데이터 미리보기",
"id_token": "ID 토큰",
"access_token": "접근 토큰",
"userinfo": "사용자 정보",
"id_token_payload": "ID 토큰 페이로드",
"access_token_payload": "접근 토큰 페이로드",
"userinfo_endpoint_response": "사용자 정보 엔드포인트 응답",
"copy": "복사",
"no_preview_data_available": "미리보기 데이터가 없습니다",
"copy_all": "모두 복사",
"preview": "미리보기",
"preview_for_user": "{name} ({email}) 미리보기",
"preview_the_oidc_data_that_would_be_sent_for_this_user": "이 사용자를 위해 전송될 OIDC 데이터 미리보기",
"show": "표시",
"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": "색상 값",
"apply": "적용",
"signup_token": "계정 생성 토큰",
"create_a_signup_token_to_allow_new_user_registration": "새로운 사용자 등록을 허용하기 위해 계정 생성 토큰을 생성합니다.",
"usage_limit": "사용량 제한",
"number_of_times_token_can_be_used": "계정 생성 토큰을 사용할 수 있는 횟수.",
"expires": "만료일",
"signup": "계정 생성",
"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": "계정 생성 기능이 활성화됩니다.",
"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": "만료됨",
"used_up": "사용 완료",
"active": "활성",
"usage": "사용량",
"created": "생성일",
"token": "토큰",
"loading": "불러오는 중",
"delete_signup_token": "계정 생성 토큰 삭제",
"are_you_sure_you_want_to_delete_this_signup_token": "이 계정 생성 토큰을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"signup_disabled_description": "계정 생성이 완전히 비활성화됩니다. 새로운 사용자 계정은 관리자만 생성할 수 있습니다.",
"signup_with_token": "토큰으로 계정 생성",
"signup_with_token_description": "사용자는 관리자가 생성한 유효한 계정 생성 토큰을 사용해야 가입할 수 있습니다.",
"signup_open": "계정 생성 허용",
"signup_open_description": "누구나 제한 없이 새로운 계정을 생성할 수 있습니다.",
"of": "의",
"skip_passkey_setup": "패스키 설정 건너뛰기",
"skip_passkey_setup_description": "패스키를 설정하는 것이 강력히 권장됩니다. 패스키가 없으면 세션이 만료되자마자 계정에 접근할 수 없게 됩니다.",
"my_apps": "내 앱",
"no_apps_available": "사용 가능한 앱이 없습니다",
"contact_your_administrator_for_app_access": "관리자에게 연락하여 앱의 접근 권한을 얻으세요.",
"launch": "실행",
"client_launch_url": "클라이언트 실행 URL",
"client_launch_url_description": "사용자가 '내 앱' 페이지에서 앱을 실행할 때 열릴 URL입니다.",
"client_name_description": "Pocket ID UI에 표시되는 클라이언트의 이름입니다.",
"revoke_access": "접근 권한 취소",
"revoke_access_description": "<b>{clientName}</b>의 접근 권한을 취소합니다. <b>{clientName}</b>은 더 이상 계정 정보에 접근할 수 없습니다.",
"revoke_access_successful": "{clientName}의 접근이 성공적으로 취소되었습니다.",
"last_signed_in_ago": "{time} 전에 로그인함"
}

View File

@@ -3,17 +3,17 @@
"my_account": "Mijn account", "my_account": "Mijn account",
"logout": "Uitloggen", "logout": "Uitloggen",
"confirm": "Bevestigen", "confirm": "Bevestigen",
"docs": "Documenten", "docs": "Documentatie",
"key": "Sleutel", "key": "Sleutel",
"value": "Waarde", "value": "Waarde",
"remove_custom_claim": "Aangepaste claim verwijderen", "remove_custom_claim": "Aangepaste claim verwijderen",
"add_custom_claim": "Aangepaste claim toevoegen", "add_custom_claim": "Aangepaste claim toevoegen",
"add_another": "Voeg er nog een toe", "add_another": "Nog een toevoegen",
"select_a_date": "Selecteer een datum", "select_a_date": "Selecteer een datum",
"select_file": "Selecteer bestand", "select_file": "Selecteer bestand",
"profile_picture": "Profielfoto", "profile_picture": "Profielfoto",
"profile_picture_is_managed_by_ldap_server": "De profielfoto wordt beheerd door de LDAP-server en kan hier niet worden gewijzigd.", "profile_picture_is_managed_by_ldap_server": "De profielfoto wordt beheerd door de LDAP-server en kan hier niet worden gewijzigd.",
"click_profile_picture_to_upload_custom": "Klik op de profielfoto om een aangepaste foto uit uw bestanden te uploaden.", "click_profile_picture_to_upload_custom": "Klik op de profielfoto om een aangepaste foto uit je bestanden te uploaden.",
"image_should_be_in_format": "De afbeelding moet in PNG- of JPEG-formaat zijn.", "image_should_be_in_format": "De afbeelding moet in PNG- of JPEG-formaat zijn.",
"items_per_page": "Aantal per pagina", "items_per_page": "Aantal per pagina",
"no_items_found": "Geen items gevonden", "no_items_found": "Geen items gevonden",
@@ -22,8 +22,8 @@
"copied": "Gekopieerd", "copied": "Gekopieerd",
"click_to_copy": "Klik om te kopiëren", "click_to_copy": "Klik om te kopiëren",
"something_went_wrong": "Er is iets misgegaan", "something_went_wrong": "Er is iets misgegaan",
"go_back_to_home": "Ga terug naar huis", "go_back_to_home": "Terug naar beginpagina",
"dont_have_access_to_your_passkey": "Heeft u geen toegang tot uw toegangscode?", "dont_have_access_to_your_passkey": "Heb je geen toegang tot je passkey?",
"login_background": "Inlogachtergrond", "login_background": "Inlogachtergrond",
"logo": "Logo", "logo": "Logo",
"login_code": "Inlogcode", "login_code": "Inlogcode",
@@ -42,46 +42,46 @@
"authentication_process_was_aborted": "Het authenticatieproces is afgebroken", "authentication_process_was_aborted": "Het authenticatieproces is afgebroken",
"error_occurred_with_authenticator": "Er is een fout opgetreden met de authenticator", "error_occurred_with_authenticator": "Er is een fout opgetreden met de authenticator",
"authenticator_does_not_support_discoverable_credentials": "De authenticator ondersteunt geen vindbare referenties", "authenticator_does_not_support_discoverable_credentials": "De authenticator ondersteunt geen vindbare referenties",
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen residente sleutels", "authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen vaste sleutels",
"passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd", "passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd",
"authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen", "authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen",
"authenticator_timed_out": "De authenticator is verlopen", "authenticator_timed_out": "De authenticator is verlopen",
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met uw beheerder.", "critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met de beheerder.",
"sign_in_to": "Meld u aan bij {name}", "sign_in_to": "Meld je aan bij {name}",
"client_not_found": "Client niet gevonden", "client_not_found": "Client niet gevonden",
"client_wants_to_access_the_following_information": "<b>{client}</b> wil toegang tot de volgende informatie:", "client_wants_to_access_the_following_information": "<b>{client}</b> wil toegang tot de volgende informatie:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Wilt u zich aanmelden bij <b>{client}</b> met uw {appName} account?", "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Wil je je aanmelden bij <b>{client}</b> met je {appName} account?",
"email": "E-mail", "email": "E-mail",
"view_your_email_address": "Bekijk uw e-mailadres", "view_your_email_address": "Bekijk je e-mailadres",
"profile": "Profiel", "profile": "Profiel",
"view_your_profile_information": "Bekijk uw profielgegevens", "view_your_profile_information": "Bekijk je profielgegevens",
"groups": "Groepen", "groups": "Groepen",
"view_the_groups_you_are_a_member_of": "Bekijk de groepen waarvan u lid bent", "view_the_groups_you_are_a_member_of": "Bekijk de groepen waarvan je lid bent",
"cancel": "Annuleren", "cancel": "Annuleren",
"sign_in": "Aanmelden", "sign_in": "Aanmelden",
"try_again": "Probeer het opnieuw", "try_again": "Probeer het opnieuw",
"client_logo": "Client logo", "client_logo": "Client logo",
"sign_out": "Afmelden", "sign_out": "Afmelden",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Wilt u zich afmelden bij Pocket ID met het account <b>{username}</b> ?", "do_you_want_to_sign_out_of_pocketid_with_the_account": "Wil je je afmelden bij {appName} met het account <b>{username}</b>?",
"sign_in_to_appname": "Meld u aan bij {appName}", "sign_in_to_appname": "Meld je aan bij {appName}",
"please_try_to_sign_in_again": "Probeer opnieuw in te loggen.", "please_try_to_sign_in_again": "Probeer opnieuw in te loggen.",
"authenticate_with_passkey_to_access_account": "Log in met je passkey om toegang te krijgen tot je account.", "authenticate_with_passkey_to_access_account": "Log in met uw passkey om toegang te krijgen tot uw account.",
"authenticate": "Authenticeren", "authenticate": "Authenticeren",
"please_try_again": "Probeer het opnieuw.", "please_try_again": "Probeer het opnieuw.",
"continue": "Doorgaan", "continue": "Doorgaan",
"alternative_sign_in": "Alternatieve aanmelding", "alternative_sign_in": "Alternatieve aanmelding",
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Als u geen toegang hebt tot uw passkeys, kunt u zich op een van de volgende manieren aanmelden.", "if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Als je geen toegang hebt tot je passkeys, kun je je op een van de volgende manieren aanmelden.",
"use_your_passkey_instead": "Wilt u in plaats daarvan uw toegangscode gebruiken?", "use_your_passkey_instead": "Wil je in plaats daarvan je toegangscode gebruiken?",
"email_login": "E-mail inloggen", "email_login": "E-mail inloggen",
"enter_a_login_code_to_sign_in": "Voer een inlogcode in om in te loggen.", "enter_a_login_code_to_sign_in": "Voer een inlogcode in om in te loggen.",
"request_a_login_code_via_email": "Vraag een inlogcode aan via e-mail.", "request_a_login_code_via_email": "Vraag een inlogcode aan via e-mail.",
"go_back": "Ga terug", "go_back": "Terug",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Er is een e-mail verzonden naar het opgegeven e-mailadres, indien dit in het systeem voorkomt.", "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Er is een e-mail verzonden naar het opgegeven e-mailadres, indien deze in het systeem voorkomt.",
"enter_code": "Voer code in", "enter_code": "Voer code in",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Voer uw e-mailadres in om een e-mail met een inlogcode te ontvangen.", "enter_your_email_address_to_receive_an_email_with_a_login_code": "Voer je e-mailadres in om een e-mail met een inlogcode te ontvangen.",
"your_email": "Uw e-mail", "your_email": "Je e-mail",
"submit": "Indienen", "submit": "Indienen",
"enter_the_code_you_received_to_sign_in": "Voer de code in die u hebt ontvangen om in te loggen.", "enter_the_code_you_received_to_sign_in": "Voer de code in die je hebt ontvangen om in te loggen.",
"code": "Code", "code": "Code",
"invalid_redirect_url": "Ongeldige omleidings-URL", "invalid_redirect_url": "Ongeldige omleidings-URL",
"audit_log": "Audit logboek", "audit_log": "Audit logboek",
@@ -93,52 +93,52 @@
"settings": "Instellingen", "settings": "Instellingen",
"update_pocket_id": "Pocket-ID bijwerken", "update_pocket_id": "Pocket-ID bijwerken",
"powered_by": "Aangedreven door", "powered_by": "Aangedreven door",
"see_your_account_activities_from_the_last_3_months": "Bekijk uw accountactiviteiten van de afgelopen 3 maanden.", "see_your_account_activities_from_the_last_3_months": "Bekijk je accountactiviteiten van de afgelopen 3 maanden.",
"time": "Tijd", "time": "Tijd",
"event": "Evenement", "event": "Evenement",
"approximate_location": "Geschatte locatie", "approximate_location": "Geschatte locatie",
"ip_address": "IP-adres", "ip_address": "IP-adres",
"device": "Apparaat", "device": "Apparaat",
"client": "Cliënt", "client": "Client",
"unknown": "Onbekend", "unknown": "Onbekend",
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt", "account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.", "profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
"account_settings": "Accountinstellingen", "account_settings": "Accountinstellingen",
"passkey_missing": "Passkey ontbreekt", "passkey_missing": "Passkey ontbreekt",
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat u de toegang tot uw account verliest.", "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat je de toegang tot je account verliest.",
"single_passkey_configured": "Eén enkele toegangscode geconfigureerd", "single_passkey_configured": "Eén enkele toegangscode geconfigureerd",
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat u de toegang tot uw account verliest.", "it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat je de toegang tot uw account verliest.",
"account_details": "Accountgegevens", "account_details": "Accountgegevens",
"passkeys": "Toegangscodes", "passkeys": "Toegangscodes",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.", "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee je jezelf kunt verifiëren.",
"add_passkey": "Passkey toevoegen", "add_passkey": "Passkey toevoegen",
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Maak een eenmalige inlogcode aan om in te loggen vanaf een ander apparaat zonder passkey.", "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Maak een eenmalige inlogcode aan om in te loggen vanaf een ander apparaat zonder passkey.",
"create": "Creëren", "create": "Aanmaken",
"first_name": "Voornaam", "first_name": "Voornaam",
"last_name": "Achternaam", "last_name": "Achternaam",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"save": "Opslaan", "save": "Opslaan",
"username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten", "username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld u aan met de volgende code. De code verloopt over 15 minuten.", "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld je aan met de volgende code. De code verloopt over 15 minuten.",
"or_visit": "of bezoek", "or_visit": "of bezoek",
"added_on": "Toegevoegd op", "added_on": "Toegevoegd op",
"rename": "Hernoemen", "rename": "Hernoemen",
"delete": "Verwijderen", "delete": "Verwijderen",
"are_you_sure_you_want_to_delete_this_passkey": "Weet u zeker dat u deze toegangscode wilt verwijderen?", "are_you_sure_you_want_to_delete_this_passkey": "Weet je zeker dat je deze passkey wilt verwijderen?",
"passkey_deleted_successfully": "Passkey succesvol verwijderd", "passkey_deleted_successfully": "Passkey succesvol verwijderd",
"delete_passkey_name": "Verwijder {passkeyName}", "delete_passkey_name": "Verwijder {passkeyName}",
"passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt", "passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt",
"name_passkey": "Naam Passkey", "name_passkey": "Naam passkey",
"name_your_passkey_to_easily_identify_it_later": "Geef uw toegangscode een naam, zodat u deze later gemakkelijk kunt terugvinden.", "name_your_passkey_to_easily_identify_it_later": "Geef je passkey een naam, zodat je deze later gemakkelijk kunt terugvinden.",
"create_api_key": "API-sleutel aanmaken", "create_api_key": "API-sleutel aanmaken",
"add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.", "add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.",
"add_api_key": "API-sleutel toevoegen", "add_api_key": "API-sleutel toevoegen",
"manage_api_keys": "API-sleutels beheren", "manage_api_keys": "API-sleutels beheren",
"api_key_created": "API-sleutel gemaakt", "api_key_created": "API-sleutel gemaakt",
"for_security_reasons_this_key_will_only_be_shown_once": "Om veiligheidsredenen wordt deze sleutel slechts één keer getoond. Bewaar hem veilig.", "for_security_reasons_this_key_will_only_be_shown_once": "Om veiligheidsredenen wordt deze sleutel slechts één keer getoond. Bewaar deze veilig.",
"description": "Beschrijving", "description": "Beschrijving",
"api_key": "API-sleutel", "api_key": "API-sleutel",
"close": "Dichtbij", "close": "Sluiten",
"name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.", "name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.",
"expires_at": "Verloopt op", "expires_at": "Verloopt op",
"when_this_api_key_will_expire": "Wanneer deze API-sleutel verloopt.", "when_this_api_key_will_expire": "Wanneer deze API-sleutel verloopt.",
@@ -146,30 +146,30 @@
"expiration_date_must_be_in_the_future": "Vervaldatum moet in de toekomst liggen", "expiration_date_must_be_in_the_future": "Vervaldatum moet in de toekomst liggen",
"revoke_api_key": "API-sleutel intrekken", "revoke_api_key": "API-sleutel intrekken",
"never": "Nooit", "never": "Nooit",
"revoke": "Herroepen", "revoke": "Intrekken",
"api_key_revoked_successfully": "API-sleutel succesvol ingetrokken", "api_key_revoked_successfully": "API-sleutel succesvol ingetrokken",
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet u zeker dat u de API-sleutel \" {apiKeyName} \" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet je zeker dat u de API-sleutel \"{apiKeyName}\" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.",
"last_used": "Laatst gebruikt", "last_used": "Laatst gebruikt",
"actions": "Acties", "actions": "Acties",
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt", "images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
"general": "Algemeen", "general": "Algemeen",
"configure_smtp_to_send_emails": "Zet e-mailmeldingen aan om mensen te laten weten als iemand inlogt vanaf een nieuw apparaat of een nieuwe plek.", "configure_smtp_to_send_emails": "Zet e-mailmeldingen aan om gebruikers te laten weten als iemand inlogt vanaf een nieuw apparaat of een nieuwe plek.",
"ldap": "LDAP", "ldap": "LDAP",
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configureer LDAP-instellingen om gebruikers en groepen vanaf een LDAP-server te synchroniseren.", "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configureer LDAP-instellingen om gebruikers en groepen vanaf een LDAP-server te synchroniseren.",
"images": "Afbeeldingen", "images": "Afbeeldingen",
"update": "Update", "update": "Update",
"email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt", "email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt",
"save_changes_question": "Wijzigingen opslaan?", "save_changes_question": "Wijzigingen opslaan?",
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "U moet de wijzigingen opslaan voordat u een test-e-mail verzendt. Wilt u nu opslaan?", "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Je moet de wijzigingen opslaan voordat je een test-e-mail verzendt. Wil je nu opslaan?",
"save_and_send": "Opslaan en verzenden", "save_and_send": "Opslaan en verzenden",
"test_email_sent_successfully": "Test-e-mail succesvol verzonden naar uw e-mailadres.", "test_email_sent_successfully": "Test-e-mail succesvol verzonden naar je e-mailadres.",
"failed_to_send_test_email": "Het is niet gelukt om een test-e-mail te versturen. Controleer de serverlogs voor meer informatie.", "failed_to_send_test_email": "Het is niet gelukt om een test-e-mail te versturen. Controleer de serverlogs voor meer informatie.",
"smtp_configuration": "SMTP-configuratie", "smtp_configuration": "SMTP-configuratie",
"smtp_host": "SMTP-host", "smtp_host": "SMTP-host",
"smtp_port": "SMTP-poort", "smtp_port": "SMTP-poort",
"smtp_user": "SMTP-gebruiker", "smtp_user": "SMTP-gebruiker",
"smtp_password": "SMTP-wachtwoord", "smtp_password": "SMTP-wachtwoord",
"smtp_from": "SMTP van", "smtp_from": "SMTP-afzender",
"smtp_tls_option": "SMTP TLS-optie", "smtp_tls_option": "SMTP TLS-optie",
"email_tls_option": "E-mail TLS-optie", "email_tls_option": "E-mail TLS-optie",
"skip_certificate_verification": "Certificaatverificatie overslaan", "skip_certificate_verification": "Certificaatverificatie overslaan",
@@ -178,15 +178,15 @@
"email_login_notification": "E-mail-inlogmelding", "email_login_notification": "E-mail-inlogmelding",
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Stuur een e-mail naar de gebruiker wanneer deze zich aanmeldt vanaf een nieuw apparaat.", "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Stuur een e-mail naar de gebruiker wanneer deze zich aanmeldt vanaf een nieuw apparaat.",
"emai_login_code_requested_by_user": "E-mail login code aangevraagd door gebruiker", "emai_login_code_requested_by_user": "E-mail login code aangevraagd door gebruiker",
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers wachtwoorden omzeilen door een inlogcode aan te vragen die naar hun e-mail wordt gestuurd. Dit maakt het een stuk minder veilig, omdat iedereen die toegang heeft tot de e-mail van de gebruiker binnen kan komen.", "allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers passkeys omzeilen door een inlogcode aan te vragen die naar hun e-mail wordt gestuurd. Dit maakt het inloggen een stuk minder veilig, omdat iedereen die toegang heeft tot de e-mail van de gebruiker binnen kan komen.",
"email_login_code_from_admin": "E-mail inlogcode van beheerder", "email_login_code_from_admin": "E-mail inlogcode van beheerder",
"allows_an_admin_to_send_a_login_code_to_the_user": "Hiermee kan een admin een inlogcode naar de gebruiker mailen.", "allows_an_admin_to_send_a_login_code_to_the_user": "Hiermee kan een beheerder een inlogcode naar de gebruiker mailen.",
"send_test_email": "Test-e-mail verzenden", "send_test_email": "Test-e-mail verzenden",
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt", "application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
"application_name": "Toepassingsnaam", "application_name": "Toepassingsnaam",
"session_duration": "Sessieduur", "session_duration": "Sessieduur",
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "De duur van een sessie in minuten voordat de gebruiker zich opnieuw moet aanmelden.", "the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "De duur van een sessie in minuten voordat de gebruiker zich opnieuw moet aanmelden.",
"enable_self_account_editing": "Zelf-accountbewerking inschakelen", "enable_self_account_editing": "Bewerken van eigen account mogelijk maken",
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Of gebruikers hun eigen accountgegevens moeten kunnen bewerken.", "whether_the_users_should_be_able_to_edit_their_own_account_details": "Of gebruikers hun eigen accountgegevens moeten kunnen bewerken.",
"emails_verified": "E-mails geverifieerd", "emails_verified": "E-mails geverifieerd",
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Of het e-mailadres van de gebruiker als geverifieerd moet worden gemarkeerd voor de OIDC-clients.", "whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Of het e-mailadres van de gebruiker als geverifieerd moet worden gemarkeerd voor de OIDC-clients.",
@@ -198,26 +198,26 @@
"ldap_bind_dn": "LDAP Bind-DN", "ldap_bind_dn": "LDAP Bind-DN",
"ldap_bind_password": "LDAP Bind-wachtwoord", "ldap_bind_password": "LDAP Bind-wachtwoord",
"ldap_base_dn": "LDAP-basis-DN", "ldap_base_dn": "LDAP-basis-DN",
"user_search_filter": "Gebruikerszoekfilter", "user_search_filter": "Zoekfilter gebruikers",
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee u gebruikers kunt zoeken/synchroniseren.", "the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee je gebruikers kunt zoeken/synchroniseren.",
"groups_search_filter": "Groepen Zoekfilter", "groups_search_filter": "Zoekfilter groepen",
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee u groepen kunt zoeken/synchroniseren.", "the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee je groepen kunt zoeken/synchroniseren.",
"attribute_mapping": "Attribuuttoewijzing", "attribute_mapping": "Attribuuttoewijzing",
"user_unique_identifier_attribute": "Gebruiker uniek identificatiekenmerk", "user_unique_identifier_attribute": "Uniek gebruikersidentificatie attribuut",
"the_value_of_this_attribute_should_never_change": "De waarde van dit kenmerk mag nooit veranderen.", "the_value_of_this_attribute_should_never_change": "De waarde van dit attribuut mag nooit veranderen.",
"username_attribute": "Gebruikersnaam Attribuut", "username_attribute": "Gebruikersnaam attribuut",
"user_mail_attribute": "Gebruikersmailkenmerk", "user_mail_attribute": "Gebruikers e-mail attribuut",
"user_first_name_attribute": "Gebruikersvoornaam Attribuut", "user_first_name_attribute": "Gebruikers voornaam attribuut",
"user_last_name_attribute": "Gebruikersnaam Achternaam Attribuut", "user_last_name_attribute": "Gebruikers achternaam attribuut",
"user_profile_picture_attribute": "Gebruikersprofielfoto-attribuut", "user_profile_picture_attribute": "Gebruikers profielfoto attribuut",
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "De waarde van dit kenmerk kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.", "the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "De waarde van dit attribuut kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.",
"group_members_attribute": "Groepsleden Attribuut", "group_members_attribute": "Groepsleden attribuut",
"the_attribute_to_use_for_querying_members_of_a_group": "Het kenmerk dat gebruikt moet worden om leden van een groep te bevragen.", "the_attribute_to_use_for_querying_members_of_a_group": "Het attribuut dat gebruikt moet worden om leden van een groep te bevragen.",
"group_unique_identifier_attribute": "Groeps uniek identificatiekenmerk", "group_unique_identifier_attribute": "Uniek groepsidentificatie attribuut",
"group_name_attribute": "Groepsnaam Attribuut", "group_name_attribute": "Groepsnaam attribuut",
"admin_group_name": "Naam van beheerdersgroep", "admin_group_name": "Naam van beheerdersgroep",
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Leden van deze groep hebben beheerdersrechten in Pocket ID.", "members_of_this_group_will_have_admin_privileges_in_pocketid": "Leden van deze groep hebben beheerdersrechten in Pocket ID.",
"disable": "Uitzetten", "disable": "Uitschakelen",
"sync_now": "Nu synchroniseren", "sync_now": "Nu synchroniseren",
"enable": "Inschakelen", "enable": "Inschakelen",
"user_created_successfully": "Gebruiker succesvol aangemaakt", "user_created_successfully": "Gebruiker succesvol aangemaakt",
@@ -228,7 +228,7 @@
"admin_privileges": "Beheerdersrechten", "admin_privileges": "Beheerdersrechten",
"admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.", "admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.",
"delete_firstname_lastname": "Verwijderen {firstName} {lastName}", "delete_firstname_lastname": "Verwijderen {firstName} {lastName}",
"are_you_sure_you_want_to_delete_this_user": "Weet u zeker dat u deze gebruiker wilt verwijderen?", "are_you_sure_you_want_to_delete_this_user": "Weet je zeker dat u deze gebruiker wilt verwijderen?",
"user_deleted_successfully": "Gebruiker succesvol verwijderd", "user_deleted_successfully": "Gebruiker succesvol verwijderd",
"role": "Rol", "role": "Rol",
"source": "Bron", "source": "Bron",
@@ -236,7 +236,7 @@
"user": "Gebruiker", "user": "Gebruiker",
"local": "Lokaal", "local": "Lokaal",
"toggle_menu": "Menu wisselen", "toggle_menu": "Menu wisselen",
"edit": "Bewerking", "edit": "Bewerk",
"user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt", "user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt",
"user_updated_successfully": "Gebruiker succesvol bijgewerkt", "user_updated_successfully": "Gebruiker succesvol bijgewerkt",
"custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt", "custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt",
@@ -252,9 +252,9 @@
"manage_user_groups": "Gebruikersgroepen beheren", "manage_user_groups": "Gebruikersgroepen beheren",
"friendly_name": "Vriendelijke naam", "friendly_name": "Vriendelijke naam",
"name_that_will_be_displayed_in_the_ui": "Naam die in de gebruikersinterface wordt weergegeven", "name_that_will_be_displayed_in_the_ui": "Naam die in de gebruikersinterface wordt weergegeven",
"name_that_will_be_in_the_groups_claim": "Naam die in de claim 'groepen' zal staan", "name_that_will_be_in_the_groups_claim": "Naam die in de claim 'groups' zal staan",
"delete_name": "Verwijder {name}", "delete_name": "Verwijder {name}",
"are_you_sure_you_want_to_delete_this_user_group": "Weet u zeker dat u deze gebruikersgroep wilt verwijderen?", "are_you_sure_you_want_to_delete_this_user_group": "Weet je zeker dat je deze gebruikersgroep wilt verwijderen?",
"user_group_deleted_successfully": "Gebruikersgroep succesvol verwijderd", "user_group_deleted_successfully": "Gebruikersgroep succesvol verwijderd",
"user_count": "Gebruikersaantal", "user_count": "Gebruikersaantal",
"user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt", "user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt",
@@ -268,19 +268,19 @@
"add_oidc_client": "OIDC-client toevoegen", "add_oidc_client": "OIDC-client toevoegen",
"manage_oidc_clients": "OIDC-clients beheren", "manage_oidc_clients": "OIDC-clients beheren",
"one_time_link": "Eenmalige link", "one_time_link": "Eenmalige link",
"use_this_link_to_sign_in_once": "Gebruik deze link om u eenmalig aan te melden. Dit is nodig voor gebruikers die nog geen passkey hebben toegevoegd of deze kwijt zijn.", "use_this_link_to_sign_in_once": "Gebruik deze link om eenmalig aan te melden. Dit is nodig voor gebruikers die nog geen passkey hebben toegevoegd of deze kwijt zijn.",
"add": "Toevoegen", "add": "Toevoegen",
"callback_urls": "Callback-URL's", "callback_urls": "Callback-URL's",
"logout_callback_urls": "Callback-URL's voor afmelden", "logout_callback_urls": "Callback-URL's voor afmelden",
"public_client": "Publieke client", "public_client": "Publieke client",
"public_clients_description": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als uw client een SPA of mobiele app is.", "public_clients_description": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als je client een SPA of mobiele app is.",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.",
"name_logo": "{name} logo", "name_logo": "{name} logo",
"change_logo": "Logo wijzigen", "change_logo": "Logo wijzigen",
"upload_logo": "Logo uploaden", "upload_logo": "Logo uploaden",
"remove_logo": "Logo verwijderen", "remove_logo": "Logo verwijderen",
"are_you_sure_you_want_to_delete_this_oidc_client": "Weet u zeker dat u deze OIDC-client wilt verwijderen?", "are_you_sure_you_want_to_delete_this_oidc_client": "Weet je zeker dat je deze OIDC-client wilt verwijderen?",
"oidc_client_deleted_successfully": "OIDC-client succesvol verwijderd", "oidc_client_deleted_successfully": "OIDC-client succesvol verwijderd",
"authorization_url": "Autorisatie-URL", "authorization_url": "Autorisatie-URL",
"oidc_discovery_url": "OIDC-ontdekkings-URL", "oidc_discovery_url": "OIDC-ontdekkings-URL",
@@ -292,12 +292,12 @@
"disabled": "Uitgeschakeld", "disabled": "Uitgeschakeld",
"oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt", "oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt",
"create_new_client_secret": "Nieuw clientgeheim aanmaken", "create_new_client_secret": "Nieuw clientgeheim aanmaken",
"are_you_sure_you_want_to_create_a_new_client_secret": "Weet u zeker dat u een nieuw client secret wilt aanmaken? De oude wordt ongeldig.", "are_you_sure_you_want_to_create_a_new_client_secret": "Weet je zeker dat je een nieuw client secret wilt aanmaken? De oude wordt ongeldig.",
"generate": "Genereren", "generate": "Genereren",
"new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt", "new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt",
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt", "allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
"oidc_client_name": "OIDC-client {name}", "oidc_client_name": "OIDC-client {name}",
"client_id": "Client id", "client_id": "Client ID",
"client_secret": "Client geheim", "client_secret": "Client geheim",
"show_more_details": "Meer details weergeven", "show_more_details": "Meer details weergeven",
"allowed_user_groups": "Toegestane gebruikersgroepen", "allowed_user_groups": "Toegestane gebruikersgroepen",
@@ -308,12 +308,12 @@
"background_image": "Achtergrondfoto", "background_image": "Achtergrondfoto",
"language": "Taal", "language": "Taal",
"reset_profile_picture_question": "Profielfoto opnieuw instellen?", "reset_profile_picture_question": "Profielfoto opnieuw instellen?",
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Hiermee wordt de geüploade afbeelding verwijderd en wordt de profielfoto teruggezet naar de standaardinstelling. Wil je doorgaan?", "this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Hiermee wordt de geüploade afbeelding verwijderd en wordt de profielfoto teruggezet naar de standaardinstelling. Wilt u doorgaan?",
"reset": "Opnieuw instellen", "reset": "Opnieuw instellen",
"reset_to_default": "Standaardinstellingen herstellen", "reset_to_default": "Standaardinstellingen herstellen",
"profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.", "profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
"select_the_language_you_want_to_use": "Kies de taal die je wilt gebruiken. Let op: sommige teksten worden automatisch vertaald en kunnen onnauwkeurig zijn.", "select_the_language_you_want_to_use": "Kies de taal die u wilt gebruiken. Let op: sommige teksten worden automatisch vertaald en kunnen onnauwkeurig zijn.",
"contribute_to_translation": "Als je een fout vindt, kun je altijd helpen met de vertaling op <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.", "contribute_to_translation": "Als u een fout vindt, kun je altijd helpen met de vertaling op <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
"personal": "Persoonlijk", "personal": "Persoonlijk",
"global": "Globaal", "global": "Globaal",
"all_users": "Alle gebruikers", "all_users": "Alle gebruikers",
@@ -325,33 +325,33 @@
"token_sign_in": "Inloggen met token", "token_sign_in": "Inloggen met token",
"client_authorization": "Client autorisatie", "client_authorization": "Client autorisatie",
"new_client_authorization": "Nieuwe clientautorisatie", "new_client_authorization": "Nieuwe clientautorisatie",
"disable_animations": "Animatie uitzetten", "disable_animations": "Animaties uitzetten",
"turn_off_ui_animations": "Zet alle animaties in de gebruikersinterface uit.", "turn_off_ui_animations": "Zet alle animaties in de gebruikersinterface uit.",
"user_disabled": "Account uitgeschakeld", "user_disabled": "Account uitgeschakeld",
"disabled_users_cannot_log_in_or_use_services": "Gebruikers met een handicap kunnen niet inloggen of diensten gebruiken.", "disabled_users_cannot_log_in_or_use_services": "Uitgeschakelde gebruikers kunnen niet inloggen of diensten gebruiken.",
"user_disabled_successfully": "Je bent nu uitgelogd.", "user_disabled_successfully": "Gebruiker is succesvol uitgeschakeld.",
"user_enabled_successfully": "Je bent nu aangemeld.", "user_enabled_successfully": "Gebruiker is succesvol geactiveerd.",
"status": "Status", "status": "Status",
"disable_firstname_lastname": "{firstName} {lastName}uitschakelen", "disable_firstname_lastname": "{firstName} {lastName} uitschakelen",
"are_you_sure_you_want_to_disable_this_user": "Weet je zeker dat je deze gebruiker wilt uitschakelen? Ze kunnen dan niet meer inloggen of diensten gebruiken.", "are_you_sure_you_want_to_disable_this_user": "Weet u zeker dat u deze gebruiker wilt uitschakelen? Deze kan dan niet meer inloggen of diensten gebruiken.",
"ldap_soft_delete_users": "Voorkom dat gebruikers met een handicap toegang krijgen tot LDAP.", "ldap_soft_delete_users": "Voorkom dat in LDAP uitgeschakelde gebruikers toegang krijgen.",
"ldap_soft_delete_users_description": "Als dit is ingeschakeld, worden gebruikers die uit LDAP worden verwijderd, uitgeschakeld in plaats van uit het systeem verwijderd.", "ldap_soft_delete_users_description": "Als dit is ingeschakeld, worden gebruikers die uit LDAP worden verwijderd, uitgeschakeld in plaats van daadwerkelijk uit het systeem verwijderd.",
"login_code_email_success": "De inlogcode is naar je gestuurd.", "login_code_email_success": "De inlogcode is naar de gebruiker gestuurd.",
"send_email": "E-mail sturen", "send_email": "Verstuur e-mail",
"show_code": "Code tonen", "show_code": "Toon code",
"callback_url_description": "URL's die je klant heeft gegeven. Als je dit leeg laat, worden ze automatisch toegevoegd. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je die beter niet doen.", "callback_url_description": "URL's die de client heeft aangegeven. Als je dit leeg laat, worden ze automatisch toegevoegd. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je dat beter niet doen.",
"logout_callback_url_description": "URL's die je klant heeft gegeven om uit te loggen. Je kunt jokertekens (*) gebruiken, maar dat is niet zo'n goed idee voor de veiligheid.", "logout_callback_url_description": "URL's die uw client heeft aangegeven om uit te loggen. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je dat beter niet doen.",
"api_key_expiration": "API-sleutel verloopt", "api_key_expiration": "API-sleutel verloopt",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Stuur een mailtje naar de gebruiker als hun API-sleutel bijna afloopt.", "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Stuur een e-mail naar de gebruiker als de geldigheid van hun API-sleutel bijna verloopt.",
"authorize_device": "Apparaat autoriseren", "authorize_device": "Apparaat autoriseren",
"the_device_has_been_authorized": "Het apparaat is goedgekeurd.", "the_device_has_been_authorized": "Het apparaat is goedgekeurd.",
"enter_code_displayed_in_previous_step": "Voer de code in die je in de vorige stap hebt gezien.", "enter_code_displayed_in_previous_step": "Voer de code in die in de vorige stap werd getoond.",
"authorize": "Autoriseren", "authorize": "Autoriseren",
"federated_client_credentials": "Federatieve clientreferenties", "federated_client_credentials": "Federatieve clientreferenties",
"federated_client_credentials_description": "Met federatieve clientreferenties kun je OIDC-clients verifiëren met JWT-tokens die zijn uitgegeven door andere instanties.", "federated_client_credentials_description": "Met federatieve clientreferenties kunt u OIDC-clients verifiëren met JWT-tokens die zijn uitgegeven door andere instanties.",
"add_federated_client_credential": "Federatieve clientreferenties toevoegen", "add_federated_client_credential": "Federatieve clientreferenties toevoegen",
"add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe", "add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe",
"oidc_allowed_group_count": "Toegestaan aantal groepen", "oidc_allowed_group_count": "Aantal groepen met toegang",
"unrestricted": "Onbeperkt", "unrestricted": "Onbeperkt",
"show_advanced_options": "Geavanceerde opties weergeven", "show_advanced_options": "Geavanceerde opties weergeven",
"hide_advanced_options": "Verberg geavanceerde opties", "hide_advanced_options": "Verberg geavanceerde opties",
@@ -378,56 +378,57 @@
"custom_accent_color": "Aangepaste accentkleur", "custom_accent_color": "Aangepaste accentkleur",
"custom_accent_color_description": "Voer een eigen kleur in met een geldige CSS-kleurcode (bijvoorbeeld hex, rgb, hsl).", "custom_accent_color_description": "Voer een eigen kleur in met een geldige CSS-kleurcode (bijvoorbeeld hex, rgb, hsl).",
"color_value": "Kleurwaarde", "color_value": "Kleurwaarde",
"apply": "Solliciteren", "apply": "Toepassen",
"signup_token": "Aanmeldingstoken", "signup_token": "Aanmeldingstoken",
"create_a_signup_token_to_allow_new_user_registration": "Maak een aanmeldingstoken aan om nieuwe gebruikers te laten registreren.", "create_a_signup_token_to_allow_new_user_registration": "Maak een aanmeldingstoken aan om nieuwe gebruikers te laten registreren.",
"usage_limit": "Gebruikslimiet", "usage_limit": "Gebruikslimiet",
"number_of_times_token_can_be_used": "Hoe vaak je het aanmeldingstoken kunt gebruiken.", "number_of_times_token_can_be_used": "Hoe vaak het aanmeldingstoken gebruikt kan worden.",
"expires": "Verloopt", "expires": "Verloopt",
"signup": "Aanmelden", "signup": "Aanmelden",
"signup_requires_valid_token": "Je hebt een geldige registratietoken nodig om een account aan te maken.", "signup_requires_valid_token": "U heeft een geldige registratietoken nodig om een account aan te maken.",
"validating_signup_token": "Inlogtoken checken", "validating_signup_token": "Inlogtoken checken",
"go_to_login": "Ga naar inloggen", "go_to_login": "Ga naar inloggen",
"signup_to_appname": "Meld je aan voor {appName}", "signup_to_appname": "Meld u aan voor {appName}",
"create_your_account_to_get_started": "Maak je account aan om te beginnen.", "create_your_account_to_get_started": "Om te beginnen moet u een account aanmaken.",
"initial_account_creation_description": "Maak een account aan om te beginnen. Je kunt later een wachtwoord instellen.", "initial_account_creation_description": "Maak een account aan om te beginnen. U kunt later een wachtwoord instellen.",
"setup_your_passkey": "Stel je passkey in", "setup_your_passkey": "Stel uw passkey in",
"create_a_passkey_to_securely_access_your_account": "Maak een toegangscode aan om veilig toegang te krijgen tot je account. Dit wordt je belangrijkste manier om in te loggen.", "create_a_passkey_to_securely_access_your_account": "Maak een toegangscode aan om veilig toegang te krijgen tot je account. Dit is je primaire manier om in te loggen.",
"skip_for_now": "Voor nu even overslaan", "skip_for_now": "Voor nu even overslaan",
"account_created": "Account aangemaakt", "account_created": "Account aangemaakt",
"enable_user_signups": "Gebruikersregistratie inschakelen", "enable_user_signups": "Gebruikersregistratie inschakelen",
"enable_user_signups_description": "Of de functie voor gebruikersregistratie moet worden ingeschakeld.", "enable_user_signups_description": "Of de functie voor gebruikersregistratie moet worden ingeschakeld.",
"user_signups_are_disabled": "Je kunt nu niet aanmelden.", "user_signups_are_disabled": "Gebruikersregistraties zijn nu uitgeschakeld.",
"create_signup_token": "Aanmeldingstoken maken", "create_signup_token": "Aanmeldingstoken maken",
"view_active_signup_tokens": "Actieve aanmeldingstokens bekijken", "view_active_signup_tokens": "Actieve aanmeldingstokens bekijken",
"manage_signup_tokens": "Aanmeldingstokens beheren", "manage_signup_tokens": "Aanmeldingstokens beheren",
"view_and_manage_active_signup_tokens": "Bekijk en beheer actieve aanmeldingstokens.", "view_and_manage_active_signup_tokens": "Bekijk en beheer actieve aanmeldingstokens.",
"signup_token_deleted_successfully": "Aanmeldingstoken succesvol verwijderd.", "signup_token_deleted_successfully": "Aanmeldingstoken succesvol verwijderd.",
"expired": "Verlopen", "expired": "Verlopen",
"used_up": "Opgebruikt", "used_up": "Verbruikt",
"active": "Actief", "active": "Actief",
"usage": "Gebruik", "usage": "Gebruik",
"created": "Gemaakt", "created": "Gemaakt",
"token": "Token", "token": "Token",
"loading": "Bezig met laden", "loading": "Bezig met laden",
"delete_signup_token": "Registratietoken verwijderen", "delete_signup_token": "Registratietoken verwijderen",
"are_you_sure_you_want_to_delete_this_signup_token": "Weet je zeker dat je dit aanmeldingstoken wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", "are_you_sure_you_want_to_delete_this_signup_token": "Weet u zeker dat u dit aanmeldingstoken wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.", "signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
"signup_with_token": "Aanmelden met token", "signup_with_token": "Aanmelden met token",
"signup_with_token_description": "Je kunt je alleen aanmelden met een geldige aanmeldtoken die door een beheerder is aangemaakt.", "signup_with_token_description": "U kunt zich alleen aanmelden met een geldige aanmeldtoken die door een beheerder is aangemaakt.",
"signup_open": "Open inschrijving", "signup_open": "Open inschrijving",
"signup_open_description": "Iedereen kan zonder beperkingen een nieuw account aanmaken.", "signup_open_description": "Iedereen kan zonder beperkingen een nieuw account aanmaken.",
"of": "van", "of": "van",
"skip_passkey_setup": "Pas de instellingen voor de toegangssleutel over", "skip_passkey_setup": "Sla de instellingen voor de toegangssleutel over",
"skip_passkey_setup_description": "Het is echt een aanrader om een wachtwoord in te stellen, want zonder dat word je uit je account gegooid zodra de sessie afloopt.", "skip_passkey_setup_description": "Het wordt aangeraden om een passkey in te stellen, want zonder dit kunt u niet meer inloggen zodra de sessie afloopt.",
"my_apps": "Mijn apps", "my_apps": "Mijn apps",
"no_apps_available": "Geen apps beschikbaar", "no_apps_available": "Geen apps beschikbaar",
"contact_your_administrator_for_app_access": "Neem contact op met je beheerder om toegang te krijgen tot applicaties.", "contact_your_administrator_for_app_access": "Neem contact op met de beheerder om toegang te krijgen tot applicaties.",
"launch": "Lancering", "launch": "Openen",
"client_launch_url": "URL voor lancering door klant", "client_launch_url": "URL voor openen door gebruiker",
"client_launch_url_description": "De URL die wordt geopend als iemand de app start vanaf de pagina Mijn apps.", "client_launch_url_description": "De URL die wordt geopend als iemand de app start vanaf de pagina Mijn apps.",
"client_name_description": "De naam van de klant die je in de Pocket ID-UI ziet.", "client_name_description": "De naam van de client die wordt getoond in de Pocket ID UI.",
"revoke_access": "Toegang intrekken", "revoke_access": "Toegang intrekken",
"revoke_access_description": "Toegang intrekken tot <b>{clientName}</b>. <b>{clientName}</b> kan je accountgegevens niet meer bekijken.", "revoke_access_description": "Toegang intrekken tot <b>{clientName}</b>. <b>{clientName}</b> kun je accountgegevens niet meer gebruiken.",
"revoke_access_successful": "De toegang tot {clientName} is nu echt geblokkeerd." "revoke_access_successful": "De toegang tot {clientName} is nu echt geblokkeerd.",
"last_signed_in_ago": "Laatst ingelogd {time} geleden"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "Nazwa klienta wyświetlana w interfejsie użytkownika Pocket ID.", "client_name_description": "Nazwa klienta wyświetlana w interfejsie użytkownika Pocket ID.",
"revoke_access": "Cofnij dostęp", "revoke_access": "Cofnij dostęp",
"revoke_access_description": "Cofnij dostęp do <b>{clientName}</b>. <b>{clientName}</b> nie będzie już mógł uzyskać dostępu do informacji o Twoim koncie.", "revoke_access_description": "Cofnij dostęp do <b>{clientName}</b>. <b>{clientName}</b> nie będzie już mógł uzyskać dostępu do informacji o Twoim koncie.",
"revoke_access_successful": "Dostęp do strony {clientName} został pomyślnie cofnięty." "revoke_access_successful": "Dostęp do strony {clientName} został pomyślnie cofnięty.",
"last_signed_in_ago": "Ostatnio zalogowany {time} temu"
} }

View File

@@ -429,5 +429,6 @@
"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 vai mais conseguir 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"
} }

View File

@@ -112,7 +112,7 @@
"passkeys": "Пасскеи", "passkeys": "Пасскеи",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте пасскеями, которые вы можете использовать для аутентификации себя.", "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте пасскеями, которые вы можете использовать для аутентификации себя.",
"add_passkey": "Добавить пасскей", "add_passkey": "Добавить пасскей",
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без passkey.", "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без пасскея.",
"create": "Создать", "create": "Создать",
"first_name": "Имя", "first_name": "Имя",
"last_name": "Фамилия", "last_name": "Фамилия",
@@ -419,15 +419,16 @@
"signup_open_description": "Любой может создать новую учетную запись без ограничений.", "signup_open_description": "Любой может создать новую учетную запись без ограничений.",
"of": "из", "of": "из",
"skip_passkey_setup": "Пропустить настройку пасскея", "skip_passkey_setup": "Пропустить настройку пасскея",
"skip_passkey_setup_description": "Настоятельно рекомендуется настроить passkey, так как без него вы более не сможете войти в учетную запись после истечения сессии.", "skip_passkey_setup_description": "Настоятельно рекомендуется настроить пасскей, так как без него вы более не сможете войти в учетную запись после истечения сессии.",
"my_apps": "Мои приложения", "my_apps": "Мои приложения",
"no_apps_available": "Нет доступных приложений", "no_apps_available": "Нет доступных приложений",
"contact_your_administrator_for_app_access": "Свяжись с администратором, чтобы получить доступ к приложениям.", "contact_your_administrator_for_app_access": "Свяжись с администратором, чтобы получить доступ к приложениям.",
"launch": "Запуск", "launch": "Запустить",
"client_launch_url": "URL запуска клиента", "client_launch_url": "Клиентский URL для запуска",
"client_launch_url_description": "URL-адрес, который откроется, когда кто-то запустит приложение со страницы «Мои приложения».", "client_launch_url_description": "URL-адрес, который откроется, когда кто-то запустит приложение со страницы «Мои приложения».",
"client_name_description": "Имя клиента, которое показывается в интерфейсе Pocket ID.", "client_name_description": "Имя клиента, которое отображается в интерфейсе Pocket ID.",
"revoke_access": "Отменить доступ", "revoke_access": "Отозвать доступ",
"revoke_access_description": "Отменить доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет заходить в твою учетную запись.", "revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.",
"revoke_access_successful": "Доступ к {clientName} был успешно заблокирован." "revoke_access_successful": "Доступ к {clientName} успешно отозван.",
"last_signed_in_ago": "Последний вход {time} назад"
} }

View File

@@ -173,7 +173,7 @@
"smtp_tls_option": "Тип SMTP TLS", "smtp_tls_option": "Тип SMTP TLS",
"email_tls_option": "TLS налаштування електронної пошти", "email_tls_option": "TLS налаштування електронної пошти",
"skip_certificate_verification": "Пропустити перевірку сертифіката", "skip_certificate_verification": "Пропустити перевірку сертифіката",
"this_can_be_useful_for_selfsigned_certificates": "Ця опція може бути корисною для спопідписних сертифікатів.", "this_can_be_useful_for_selfsigned_certificates": "Ця опція може бути корисною для самопідписаних сертифікатів.",
"enabled_emails": "Увімкнені електронні листи", "enabled_emails": "Увімкнені електронні листи",
"email_login_notification": "Сповіщення електронною поштою про вхід", "email_login_notification": "Сповіщення електронною поштою про вхід",
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Надіслати електронний лист користувачеві після входу з нового пристрою.", "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Надіслати електронний лист користувачеві після входу з нового пристрою.",
@@ -196,7 +196,7 @@
"client_configuration": "Налаштування клієнтів", "client_configuration": "Налаштування клієнтів",
"ldap_url": "URL-адреса LDAP", "ldap_url": "URL-адреса LDAP",
"ldap_bind_dn": "LDAP Bind DN", "ldap_bind_dn": "LDAP Bind DN",
"ldap_bind_password": "Пароль прив’язки LDAP", "ldap_bind_password": "Пароль LDAP Bind",
"ldap_base_dn": "LDAP Base DN", "ldap_base_dn": "LDAP Base DN",
"user_search_filter": "Фільтр пошуку користувачів", "user_search_filter": "Фільтр пошуку користувачів",
"the_search_filter_to_use_to_search_or_sync_users": "Фільтр пошуку для пошуку/синхронізації користувачів.", "the_search_filter_to_use_to_search_or_sync_users": "Фільтр пошуку для пошуку/синхронізації користувачів.",
@@ -251,8 +251,8 @@
"add_group": "Створити групу", "add_group": "Створити групу",
"manage_user_groups": "Керування групами користувачів", "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": "Ім'я, яке буде в атрибуті \"groups\"", "name_that_will_be_in_the_groups_claim": "Назва, яка буде в атрибуті \"groups\"",
"delete_name": "Видалити {name}", "delete_name": "Видалити {name}",
"are_you_sure_you_want_to_delete_this_user_group": "Ви впевнені, що хочете видалити цю групу користувачів?", "are_you_sure_you_want_to_delete_this_user_group": "Ви впевнені, що хочете видалити цю групу користувачів?",
"user_group_deleted_successfully": "Групу користувачів успішно видалено", "user_group_deleted_successfully": "Групу користувачів успішно видалено",
@@ -420,14 +420,15 @@
"of": "з", "of": "з",
"skip_passkey_setup": "Пропустити налаштування ключа доступу", "skip_passkey_setup": "Пропустити налаштування ключа доступу",
"skip_passkey_setup_description": "Рекомендується налаштувати ключ доступу, оскільки без нього ви не зможете увійти у свій обліковий запис після закінчення сеансу.", "skip_passkey_setup_description": "Рекомендується налаштувати ключ доступу, оскільки без нього ви не зможете увійти у свій обліковий запис після закінчення сеансу.",
"my_apps": "Мої програми", "my_apps": "Мої додатки",
"no_apps_available": "Немає доступних додатків", "no_apps_available": "Немає доступних додатків",
"contact_your_administrator_for_app_access": "Зверніться до адміністратора, щоб отримати доступ до додатків.", "contact_your_administrator_for_app_access": "Зверніться до адміністратора, щоб отримати доступ до додатків.",
"launch": "Запуск", "launch": "Запуск",
"client_launch_url": "URL-адреса запуску клієнта", "client_launch_url": "URL-адреса для запуску клієнта",
"client_launch_url_description": "URL-адреса, яка відкриється, коли користувач запустить програму зі сторінки «Мої програми».", "client_launch_url_description": "URL-адреса, яка відкриється, коли користувач запустить програму зі сторінки «Мої програми».",
"client_name_description": "Ім'я клієнта, яке відображається в інтерфейсі Pocket ID.", "client_name_description": "Назва клієнта, яке відображається в інтерфейсі Pocket ID.",
"revoke_access": "Скасувати доступ", "revoke_access": "Скасувати доступ",
"revoke_access_description": "Скасувати доступ до <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.", "revoke_access_description": "Скасувати доступ для <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.",
"revoke_access_successful": "Доступ до {clientName} було успішно скасовано." "revoke_access_successful": "Доступ для {clientName} було успішно скасовано.",
"last_signed_in_ago": "Останній вхід {time} тому"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "Tên của khách hàng hiển thị trong giao diện Pocket ID.", "client_name_description": "Tên của khách hàng hiển thị trong giao diện Pocket ID.",
"revoke_access": "Hủy quyền truy cập", "revoke_access": "Hủy quyền truy cập",
"revoke_access_description": "Hủy quyền truy cập vào <b>{clientName}</b>. <b>{clientName}</b> sẽ không còn có thể truy cập thông tin tài khoản của bạn.", "revoke_access_description": "Hủy quyền truy cập vào <b>{clientName}</b>. <b>{clientName}</b> sẽ không còn có thể truy cập thông tin tài khoản của bạn.",
"revoke_access_successful": "Quyền truy cập vào {clientName} đã bị thu hồi thành công." "revoke_access_successful": "Quyền truy cập vào {clientName} đã bị thu hồi thành công.",
"last_signed_in_ago": "Lần đăng nhập cuối cùng cách đây {time}"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "在Pocket ID用户界面中显示的客户端名称。", "client_name_description": "在Pocket ID用户界面中显示的客户端名称。",
"revoke_access": "撤销访问权限", "revoke_access": "撤销访问权限",
"revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。", "revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。",
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。" "revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。",
"last_signed_in_ago": "最后一次登录 {time} 前"
} }

View File

@@ -429,5 +429,6 @@
"client_name_description": "顯示在 Pocket ID UI 中的用戶端名稱。", "client_name_description": "顯示在 Pocket ID UI 中的用戶端名稱。",
"revoke_access": "撤銷存取權", "revoke_access": "撤銷存取權",
"revoke_access_description": "撤銷存取 <b>{clientName}</b>. <b>{clientName}</b>將無法再存取您的帳戶資訊。", "revoke_access_description": "撤銷存取 <b>{clientName}</b>. <b>{clientName}</b>將無法再存取您的帳戶資訊。",
"revoke_access_successful": "{clientName} 的存取權已成功取消。" "revoke_access_successful": "{clientName} 的存取權已成功取消。",
"last_signed_in_ago": "上次登入 {time} 前"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "1.7.0", "version": "1.8.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"axios": "^1.11.0", "axios": "^1.11.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"jose": "^5.10.0", "jose": "^5.10.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sveltekit-superforms": "^2.27.1", "sveltekit-superforms": "^2.27.1",
@@ -46,6 +47,7 @@
"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",
"svelte": "^5.36.16", "svelte": "^5.36.16",
"svelte-check": "^4.3.0", "svelte-check": "^4.3.0",
"svelte-sonner": "^1.0.5", "svelte-sonner": "^1.0.5",

View File

@@ -9,6 +9,7 @@
"es", "es",
"fr", "fr",
"it", "it",
"ko",
"nl", "nl",
"pl", "pl",
"pt-BR", "pt-BR",

View File

@@ -132,6 +132,7 @@
/* Font */ /* Font */
--font-playfair: 'Playfair Display', serif; --font-playfair: 'Playfair Display', serif;
--font-code: 'Google Sans', sans-serif;
} }
@layer base { @layer base {
@@ -167,6 +168,11 @@
font-weight: 700; font-weight: 700;
src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff'); src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
} }
@font-face {
font-family: 'Google Sans';
font-weight: 600;
src: url('/fonts/GoogleSansCode-SemiBold.ttf') format('truetype');
}
} }
@keyframes accordion-down { @keyframes accordion-down {

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import * as Command from '$lib/components/ui/command';
import * as Popover from '$lib/components/ui/popover';
import { cn } from '$lib/utils/style';
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
import type { FormEventHandler } from 'svelte/elements';
type Item = {
value: string;
label: string;
};
let {
items,
selectedItems = $bindable(),
onSelect,
oninput,
isLoading = false,
placeholder = 'Select items...',
searchText = 'Search...',
noItemsText = 'No items found.',
disableInternalSearch = false,
id
}: {
items: Item[];
selectedItems: string[];
onSelect?: (value: string[]) => void;
oninput?: FormEventHandler<HTMLInputElement>;
isLoading?: boolean;
placeholder?: string;
searchText?: string;
noItemsText?: string;
disableInternalSearch?: boolean;
id?: string;
} = $props();
let open = $state(false);
let searchValue = $state('');
let filteredItems = $state(items);
const selectedLabels = $derived(
items.filter((item) => selectedItems.includes(item.value)).map((item) => item.label)
);
function handleItemSelect(value: string) {
let newSelectedItems: string[];
if (selectedItems.includes(value)) {
newSelectedItems = selectedItems.filter((item) => item !== value);
} else {
newSelectedItems = [...selectedItems, value];
}
selectedItems = newSelectedItems;
onSelect?.(newSelectedItems);
}
function filterItems(search: string) {
if (disableInternalSearch) return;
searchValue = search;
if (!search) {
filteredItems = items;
} else {
filteredItems = items.filter((item) =>
item.label.toLowerCase().includes(search.toLowerCase())
);
}
}
// Reset search value when the popover is closed
$effect(() => {
if (!open) {
filterItems('');
}
filteredItems = items;
});
</script>
<Popover.Root bind:open>
<Popover.Trigger {id}>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
role="combobox"
aria-expanded={open}
class="h-auto min-h-10 w-full justify-between"
>
<div class="flex flex-wrap items-center gap-1">
{#if selectedItems.length > 0}
{#each selectedLabels as label}
<Badge variant="secondary">{label}</Badge>
{/each}
{:else}
<span class="text-muted-foreground font-normal">{placeholder}</span>
{/if}
</div>
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="p-0" sameWidth>
<Command.Root shouldFilter={false}>
<Command.Input
placeholder={searchText}
value={searchValue}
oninput={(e) => {
filterItems(e.currentTarget.value);
oninput?.(e);
}}
/>
<Command.Empty>
{#if isLoading}
<div class="flex w-full items-center justify-center py-2">
<LoaderCircle class="size-4 animate-spin" />
</div>
{:else}
{noItemsText}
{/if}
</Command.Empty>
<Command.Group class="max-h-60 overflow-y-auto">
{#each filteredItems as item}
<Command.Item
aria-checked={selectedItems.includes(item.value)}
value={item.value}
onSelect={() => {
handleItemSelect(item.value);
}}
>
<LucideCheck
class={cn('mr-2 size-4', !selectedItems.includes(item.value) && 'text-transparent')}
/>
{item.label}
</Command.Item>
{/each}
</Command.Group>
</Command.Root>
</Popover.Content>
</Popover.Root>

View File

@@ -45,7 +45,7 @@
)}`} )}`}
class="text-muted-foreground text-xs transition-colors hover:underline" class="text-muted-foreground text-xs transition-colors hover:underline"
> >
{m.dont_have_access_to_your_passkey()} {m.alternative_sign_in_methods()}
</a> </a>
</div> </div>
{/if} {/if}
@@ -82,7 +82,7 @@
)}`} )}`}
class="text-muted-foreground mt-7 flex justify-center text-xs transition-colors hover:underline" class="text-muted-foreground mt-7 flex justify-center text-xs transition-colors hover:underline"
> >
{m.dont_have_access_to_your_passkey()} {m.alternative_sign_in_methods()}
</a> </a>
{/if} {/if}
</Card.CardContent> </Card.CardContent>

View File

@@ -36,8 +36,7 @@
async function createLoginCode() { async function createLoginCode() {
try { try {
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000); code = await userService.createOneTimeAccessToken(userId!, availableExpirations[selectedExpiration]);
code = await userService.createOneTimeAccessToken(expiration, userId!);
oneTimeLink = `${page.url.origin}/lc/${code}`; oneTimeLink = `${page.url.origin}/lc/${code}`;
} catch (e) { } catch (e) {
axiosErrorToast(e); axiosErrorToast(e);
@@ -46,8 +45,7 @@
async function sendLoginCodeEmail() { async function sendLoginCodeEmail() {
try { try {
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000); await userService.requestOneTimeAccessEmailAsAdmin(userId!, availableExpirations[selectedExpiration]);
await userService.requestOneTimeAccessEmailAsAdmin(userId!, expiration);
toast.success(m.login_code_email_success()); toast.success(m.login_code_email_success());
onOpenChange(false); onOpenChange(false);
} catch (e) { } catch (e) {
@@ -81,7 +79,7 @@
value={Object.keys(availableExpirations)[0]} value={Object.keys(availableExpirations)[0]}
onValueChange={(v) => (selectedExpiration = v! as keyof typeof availableExpirations)} onValueChange={(v) => (selectedExpiration = v! as keyof typeof availableExpirations)}
> >
<Select.Trigger id="expiration" class="h-9 w-full"> <Select.Trigger id="expiration" class="w-full h-9">
{selectedExpiration} {selectedExpiration}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
@@ -108,10 +106,10 @@
{:else} {:else}
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<CopyToClipboard value={code!}> <CopyToClipboard value={code!}>
<p class="text-3xl font-semibold">{code}</p> <p class="text-3xl font-code">{code}</p>
</CopyToClipboard> </CopyToClipboard>
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3"> <div class="flex items-center justify-center gap-3 my-2 text-muted-foreground">
<Separator /> <Separator />
<p class="text-xs text-nowrap">{m.or_visit()}</p> <p class="text-xs text-nowrap">{m.or_visit()}</p>
<Separator /> <Separator />

View File

@@ -37,8 +37,7 @@
async function createSignupToken() { async function createSignupToken() {
try { try {
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000); signupToken = await userService.createSignupToken(availableExpirations[selectedExpiration], usageLimit);
signupToken = await userService.createSignupToken(expiration, usageLimit);
signupLink = `${page.url.origin}/st/${signupToken}`; signupLink = `${page.url.origin}/st/${signupToken}`;
if (onTokenCreated) { if (onTokenCreated) {

View File

@@ -14,10 +14,15 @@ export default class AppConfigService extends APIService {
} }
async update(appConfig: AllAppConfig) { async update(appConfig: AllAppConfig) {
// Convert all values to string // Convert all values to string, stringifying JSON where needed
const appConfigConvertedToString = {}; const appConfigConvertedToString: Record<string, string> = {};
for (const key in appConfig) { for (const key in appConfig) {
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString(); const value = (appConfig as any)[key];
if (typeof value === 'object' && value !== null) {
appConfigConvertedToString[key] = JSON.stringify(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 this.parseConfigList(res.data);
@@ -66,6 +71,16 @@ export default class AppConfigService extends APIService {
} }
private parseValue(value: string) { private parseValue(value: string) {
// Try to parse JSON first
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return parsed;
}
value = String(parsed);
} catch {}
// Handle rest of the types
if (value === 'true') { if (value === 'true') {
return true; return true;
} else if (value === 'false') { } else if (value === 'false') {

View File

@@ -1,9 +1,10 @@
import type { import type {
AuthorizedOidcClient, AccessibleOidcClient,
AuthorizeResponse, AuthorizeResponse,
OidcClient, OidcClient,
OidcClientCreate, OidcClientCreate,
OidcClientMetaData, OidcClientMetaData,
OidcClientUpdate,
OidcClientWithAllowedUserGroups, OidcClientWithAllowedUserGroups,
OidcClientWithAllowedUserGroupsCount, OidcClientWithAllowedUserGroupsCount,
OidcDeviceCodeInfo OidcDeviceCodeInfo
@@ -19,7 +20,8 @@ class OidcService extends APIService {
callbackURL: string, callbackURL: string,
nonce?: string, nonce?: string,
codeChallenge?: string, codeChallenge?: string,
codeChallengeMethod?: string codeChallengeMethod?: string,
reauthenticationToken?: string
) { ) {
const res = await this.api.post('/oidc/authorize', { const res = await this.api.post('/oidc/authorize', {
scope, scope,
@@ -27,7 +29,8 @@ class OidcService extends APIService {
callbackURL, callbackURL,
clientId, clientId,
codeChallenge, codeChallenge,
codeChallengeMethod codeChallengeMethod,
reauthenticationToken
}); });
return res.data as AuthorizeResponse; return res.data as AuthorizeResponse;
@@ -65,7 +68,7 @@ class OidcService extends APIService {
return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData; return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
} }
async updateClient(id: string, client: OidcClientCreate) { async updateClient(id: string, client: OidcClientUpdate) {
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient; return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
} }
@@ -115,22 +118,16 @@ class OidcService extends APIService {
return response.data; return response.data;
} }
async listAuthorizedClients(options?: SearchPaginationSortRequest) { async listOwnAccessibleClients(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/oidc/users/me/clients', { const res = await this.api.get('/oidc/users/me/clients', {
params: options params: options
}); });
return res.data as Paginated<AuthorizedOidcClient>;
}
async listAuthorizedClientsForUser(userId: string, options?: SearchPaginationSortRequest) { return res.data as Paginated<AccessibleOidcClient>;
const res = await this.api.get(`/oidc/users/${userId}/clients`, {
params: options
});
return res.data as Paginated<AuthorizedOidcClient>;
} }
async revokeOwnAuthorizedClient(clientId: string) { async revokeOwnAuthorizedClient(clientId: string) {
await this.api.delete(`/oidc/users/me/clients/${clientId}`); await this.api.delete(`/oidc/users/me/authorized-clients/${clientId}`);
} }
} }

View File

@@ -75,17 +75,17 @@ export default class UserService extends APIService {
cachedProfilePicture.bustCache(userId); cachedProfilePicture.bustCache(userId);
} }
async createOneTimeAccessToken(expiresAt: Date, userId: string) { async createOneTimeAccessToken(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, userId,
expiresAt ttl,
}); });
return res.data.token; return res.data.token;
} }
async createSignupToken(expiresAt: Date, usageLimit: number) { async createSignupToken(ttl: string|number, usageLimit: number) {
const res = await this.api.post(`/signup-tokens`, { const res = await this.api.post(`/signup-tokens`, {
expiresAt, ttl,
usageLimit usageLimit
}); });
return res.data.token; return res.data.token;
@@ -100,8 +100,8 @@ export default class UserService extends APIService {
await this.api.post('/one-time-access-email', { email, redirectPath }); await this.api.post('/one-time-access-email', { email, redirectPath });
} }
async requestOneTimeAccessEmailAsAdmin(userId: string, expiresAt: Date) { async requestOneTimeAccessEmailAsAdmin(userId: string, ttl: string|number) {
await this.api.post(`/users/${userId}/one-time-access-email`, { expiresAt }); await this.api.post(`/users/${userId}/one-time-access-email`, { ttl });
} }
async updateUserGroups(id: string, userGroupIds: string[]) { async updateUserGroups(id: string, userGroupIds: string[]) {

View File

@@ -37,6 +37,11 @@ class WebAuthnService extends APIService {
async updateCredentialName(id: string, name: string) { async updateCredentialName(id: string, name: string) {
await this.api.patch(`/webauthn/credentials/${id}`, { name }); await this.api.patch(`/webauthn/credentials/${id}`, { name });
} }
async reauthenticate(body?: AuthenticationResponseJSON) {
const res = await this.api.post('/webauthn/reauthenticate', body);
return res.data.reauthenticationToken as string;
}
} }
export default WebAuthnService; export default WebAuthnService;

View File

@@ -4,9 +4,9 @@ import { writable } from 'svelte/store';
const userStore = writable<User | null>(null); const userStore = writable<User | null>(null);
const setUser = (user: User) => { const setUser = async (user: User) => {
if (user.locale) { if (user.locale) {
setLocale(user.locale, false); await setLocale(user.locale, false);
} }
userStore.set(user); userStore.set(user);
}; };

View File

@@ -1,3 +1,5 @@
import type { CustomClaim } from './custom-claim.type';
export type AppConfig = { export type AppConfig = {
appName: string; appName: string;
allowOwnAccountEdit: boolean; allowOwnAccountEdit: boolean;
@@ -14,6 +16,8 @@ export type AllAppConfig = AppConfig & {
// General // General
sessionDuration: number; sessionDuration: number;
emailsVerified: boolean; emailsVerified: boolean;
signupDefaultUserGroupIDs: string[];
signupDefaultCustomClaims: CustomClaim[];
// Email // Email
smtpHost: string; smtpHost: string;
smtpPort: number; smtpPort: number;

View File

@@ -4,6 +4,7 @@ export type OidcClientMetaData = {
id: string; id: string;
name: string; name: string;
hasLogo: boolean; hasLogo: boolean;
requiresReauthentication: boolean;
launchURL?: string; launchURL?: string;
}; };
@@ -23,6 +24,7 @@ export type OidcClient = OidcClientMetaData & {
logoutCallbackURLs: string[]; logoutCallbackURLs: string[];
isPublic: boolean; isPublic: boolean;
pkceEnabled: boolean; pkceEnabled: boolean;
requiresReauthentication: boolean;
credentials?: OidcClientCredentials; credentials?: OidcClientCredentials;
launchURL?: string; launchURL?: string;
}; };
@@ -35,7 +37,13 @@ export type OidcClientWithAllowedUserGroupsCount = OidcClient & {
allowedUserGroupsCount: number; allowedUserGroupsCount: number;
}; };
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>; export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
export type OidcClientCreate = OidcClientUpdate & {
id?: string;
};
export type OidcClientUpdateWithLogo = OidcClientUpdate & {
logo: File | null | undefined;
};
export type OidcClientCreateWithLogo = OidcClientCreate & { export type OidcClientCreateWithLogo = OidcClientCreate & {
logo: File | null | undefined; logo: File | null | undefined;
@@ -53,7 +61,6 @@ export type AuthorizeResponse = {
issuer: string; issuer: string;
}; };
export type AuthorizedOidcClient = { export type AccessibleOidcClient = OidcClientMetaData & {
scope: string; lastUsedAt: Date | null;
client: OidcClientMetaData;
}; };

View File

@@ -1,10 +1,26 @@
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime'; import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
import { setDefaultOptions } from 'date-fns';
import { z } from 'zod/v4'; import { z } from 'zod/v4';
export function setLocale(locale: Locale, reload = true) { export async function setLocale(locale: Locale, reload = true) {
import(`../../../node_modules/zod/v4/locales/${locale}.js`) const [zodResult, dateFnsResult] = await Promise.allSettled([
.then((zodLocale) => z.config(zodLocale.default())) import(`../../../node_modules/zod/v4/locales/${locale}.js`),
.finally(() => { import(`../../../node_modules/date-fns/locale/${locale}.js`)
setParaglideLocale(locale, { reload }); ]);
if (zodResult.status === 'fulfilled') {
z.config(zodResult.value.default());
} else {
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
}
setParaglideLocale(locale, { reload });
if (dateFnsResult.status === 'fulfilled') {
setDefaultOptions({
locale: dateFnsResult.value.default
}); });
} else {
console.warn(`Failed to load date-fns locale for ${locale}:`, dateFnsResult.reason);
}
} }

View File

@@ -1,9 +1,7 @@
import z from 'zod/v4'; import z from 'zod/v4';
export const optionalString = z export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
.string() z.preprocess((v) => (v === '' ? undefined : v), validation);
.transform((v) => (v === '' ? undefined : v))
.optional();
export const optionalUrl = z export const optionalUrl = z
.url() .url()

View File

@@ -6,8 +6,6 @@
import Header from '$lib/components/header/header.svelte'; import Header from '$lib/components/header/header.svelte';
import { Toaster } from '$lib/components/ui/sonner'; import { Toaster } from '$lib/components/ui/sonner';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store';
import userStore from '$lib/stores/user-store';
import { getAuthRedirectPath } from '$lib/utils/redirection-util'; import { getAuthRedirectPath } from '$lib/utils/redirection-util';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@@ -28,14 +26,6 @@
if (redirectPath) { if (redirectPath) {
goto(redirectPath); goto(redirectPath);
} }
if (user) {
userStore.setUser(user);
}
if (appConfig) {
appConfigStore.set(appConfig);
}
</script> </script>
{#if !appConfig} {#if !appConfig}

View File

@@ -1,5 +1,7 @@
import AppConfigService from '$lib/services/app-config-service'; import AppConfigService from '$lib/services/app-config-service';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import appConfigStore from '$lib/stores/application-configuration-store';
import userStore from '$lib/stores/user-store';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
export const ssr = false; export const ssr = false;
@@ -19,6 +21,14 @@ export const load: LayoutLoad = async () => {
const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]); const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]);
if (user) {
await userStore.setUser(user);
}
if (appConfig) {
appConfigStore.set(appConfig);
}
return { return {
user, user,
appConfig appConfig

View File

@@ -11,7 +11,7 @@
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import { getWebauthnErrorMessage } from '$lib/utils/error-util'; import { getWebauthnErrorMessage } from '$lib/utils/error-util';
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte'; import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
import { startAuthentication } from '@simplewebauthn/browser'; import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
@@ -29,6 +29,7 @@
let errorMessage: string | null = $state(null); let errorMessage: string | null = $state(null);
let authorizationRequired = $state(false); let authorizationRequired = $state(false);
let authorizationConfirmed = $state(false); let authorizationConfirmed = $state(false);
let userSignedInAt: Date | undefined;
onMount(() => { onMount(() => {
if ($userStore) { if ($userStore) {
@@ -38,13 +39,16 @@
async function authorize() { async function authorize() {
isLoading = true; isLoading = true;
let authResponse: AuthenticationResponseJSON | undefined;
try { try {
// Get access token if not signed in
if (!$userStore?.id) { if (!$userStore?.id) {
const loginOptions = await webauthnService.getLoginOptions(); const loginOptions = await webauthnService.getLoginOptions();
const authResponse = await startAuthentication({ optionsJSON: loginOptions }); authResponse = await startAuthentication({ optionsJSON: loginOptions });
const user = await webauthnService.finishLogin(authResponse); const user = await webauthnService.finishLogin(authResponse);
userStore.setUser(user); userStore.setUser(user);
userSignedInAt = new Date();
} }
if (!authorizationConfirmed) { if (!authorizationConfirmed) {
@@ -56,8 +60,28 @@
} }
} }
let reauthToken: string | undefined;
if (client?.requiresReauthentication) {
let authResponse;
const signedInRecently =
userSignedInAt && userSignedInAt.getTime() > Date.now() - 60 * 1000;
if (!signedInRecently) {
const loginOptions = await webauthnService.getLoginOptions();
authResponse = await startAuthentication({ optionsJSON: loginOptions });
}
reauthToken = await webauthnService.reauthenticate(authResponse);
}
await oidService await oidService
.authorize(client!.id, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod) .authorize(
client!.id,
scope,
callbackURL,
nonce,
codeChallenge,
codeChallengeMethod,
reauthToken
)
.then(async ({ code, callbackURL, issuer }) => { .then(async ({ code, callbackURL, issuer }) => {
onSuccess(code, callbackURL, issuer); onSuccess(code, callbackURL, issuer);
}); });

View File

@@ -45,7 +45,7 @@
const loginOptions = await webauthnService.getLoginOptions(); const loginOptions = await webauthnService.getLoginOptions();
const authResponse = await startAuthentication({ optionsJSON: loginOptions }); const authResponse = await startAuthentication({ optionsJSON: loginOptions });
const user = await webauthnService.finishLogin(authResponse); const user = await webauthnService.finishLogin(authResponse);
userStore.setUser(user); await userStore.setUser(user);
} }
const info = await oidcService.getDeviceCodeInfo(userCode); const info = await oidcService.getDeviceCodeInfo(userCode);

View File

@@ -23,7 +23,7 @@
const authResponse = await startAuthentication({ optionsJSON: loginOptions }); const authResponse = await startAuthentication({ optionsJSON: loginOptions });
const user = await webauthnService.finishLogin(authResponse); const user = await webauthnService.finishLogin(authResponse);
userStore.setUser(user); await userStore.setUser(user);
goto('/settings'); goto('/settings');
} catch (e) { } catch (e) {
error = getWebauthnErrorMessage(e); error = getWebauthnErrorMessage(e);

View File

@@ -23,7 +23,7 @@
isLoading = true; isLoading = true;
try { try {
const user = await userService.exchangeOneTimeAccessToken(code); const user = await userService.exchangeOneTimeAccessToken(code);
userStore.setUser(user); await userStore.setUser(user);
try { try {
goto(data.redirect); goto(data.redirect);

View File

@@ -16,6 +16,7 @@
es: 'Español', es: 'Español',
fr: 'Français', fr: 'Français',
it: 'Italiano', it: 'Italiano',
ko: '한국어',
nl: 'Nederlands', nl: 'Nederlands',
pl: 'Polski', pl: 'Polski',
'pt-BR': 'Português brasileiro', 'pt-BR': 'Português brasileiro',
@@ -31,7 +32,7 @@
...$userStore!, ...$userStore!,
locale locale
}); });
setLocale(locale); await setLocale(locale);
} }
</script> </script>

View File

@@ -22,9 +22,8 @@
$effect(() => { $effect(() => {
if (show) { if (show) {
const expiration = new Date(Date.now() + 15 * 60 * 1000);
userService userService
.createOneTimeAccessToken(expiration, 'me') .createOneTimeAccessToken('me')
.then((c) => { .then((c) => {
code = c; code = c;
loginCodeLink = page.url.origin + '/lc/' + code; loginCodeLink = page.url.origin + '/lc/' + code;
@@ -52,9 +51,9 @@
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<CopyToClipboard value={code!}> <CopyToClipboard value={code!}>
<p class="text-3xl font-semibold">{code}</p> <p class="text-3xl font-code">{code}</p>
</CopyToClipboard> </CopyToClipboard>
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3"> <div class="flex items-center justify-center gap-3 my-2 text-muted-foreground">
<Separator /> <Separator />
<p class="text-xs text-nowrap">{m.or_visit()}</p> <p class="text-xs text-nowrap">{m.or_visit()}</p>
<Separator /> <Separator />

View File

@@ -5,7 +5,7 @@
import type { ApiKeyCreate } from '$lib/types/api-key.type'; import type { ApiKeyCreate } from '$lib/types/api-key.type';
import { preventDefault } from '$lib/utils/event-util'; import { preventDefault } from '$lib/utils/event-util';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
import { optionalString } from '$lib/utils/zod-util'; import { emptyToUndefined } from '$lib/utils/zod-util';
import { z } from 'zod/v4'; import { z } from 'zod/v4';
let { let {
@@ -28,7 +28,7 @@
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(3).max(50), name: z.string().min(3).max(50),
description: optionalString, description: emptyToUndefined(z.string().optional()),
expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future()) expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future())
}); });

View File

@@ -5,11 +5,12 @@
import appConfigStore from '$lib/stores/application-configuration-store'; import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration'; import type { AllAppConfig } from '$lib/types/application-configuration';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { LucideImage, Mail, SlidersHorizontal, UserSearch } from '@lucide/svelte'; import { LucideImage, Mail, SlidersHorizontal, UserSearch, Users } from '@lucide/svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import AppConfigEmailForm from './forms/app-config-email-form.svelte'; import AppConfigEmailForm from './forms/app-config-email-form.svelte';
import AppConfigGeneralForm from './forms/app-config-general-form.svelte'; import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte'; import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
import AppConfigSignupDefaultsForm from './forms/app-config-signup-defaults-form.svelte';
import UpdateApplicationImages from './update-application-images.svelte'; import UpdateApplicationImages from './update-application-images.svelte';
let { data } = $props(); let { data } = $props();
@@ -68,6 +69,17 @@
</CollapsibleCard> </CollapsibleCard>
</div> </div>
<div>
<CollapsibleCard
id="application-configuration-signup-defaults"
icon={Users}
title={m.user_creation()}
description={m.configure_user_creation()}
>
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
</CollapsibleCard>
</div>
<div> <div>
<CollapsibleCard <CollapsibleCard
id="application-configuration-email" id="application-configuration-email"

View File

@@ -23,27 +23,11 @@
let isLoading = $state(false); let isLoading = $state(false);
const signupOptions = {
disabled: {
label: m.disabled(),
description: m.signup_disabled_description()
},
withToken: {
label: m.signup_with_token(),
description: m.signup_with_token_description()
},
open: {
label: m.signup_open(),
description: m.signup_open_description()
}
};
const updatedAppConfig = { const updatedAppConfig = {
appName: appConfig.appName, appName: appConfig.appName,
sessionDuration: appConfig.sessionDuration, sessionDuration: appConfig.sessionDuration,
emailsVerified: appConfig.emailsVerified, emailsVerified: appConfig.emailsVerified,
allowOwnAccountEdit: appConfig.allowOwnAccountEdit, allowOwnAccountEdit: appConfig.allowOwnAccountEdit,
allowUserSignups: appConfig.allowUserSignups,
disableAnimations: appConfig.disableAnimations, disableAnimations: appConfig.disableAnimations,
accentColor: appConfig.accentColor accentColor: appConfig.accentColor
}; };
@@ -53,7 +37,6 @@
sessionDuration: z.number().min(1).max(43200), sessionDuration: z.number().min(1).max(43200),
emailsVerified: z.boolean(), emailsVerified: z.boolean(),
allowOwnAccountEdit: z.boolean(), allowOwnAccountEdit: z.boolean(),
allowUserSignups: z.enum(['disabled', 'withToken', 'open']),
disableAnimations: z.boolean(), disableAnimations: z.boolean(),
accentColor: z.string() accentColor: z.string()
}); });
@@ -80,55 +63,6 @@
description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()} description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()}
bind:input={$inputs.sessionDuration} bind:input={$inputs.sessionDuration}
/> />
<div class="grid gap-2">
<div>
<Label class="mb-0" for="enable-user-signup">{m.enable_user_signups()}</Label>
<p class="text-muted-foreground text-[0.8rem]">
{m.enable_user_signups_description()}
</p>
</div>
<Select.Root
disabled={$appConfigStore.uiConfigDisabled}
type="single"
value={$inputs.allowUserSignups.value}
onValueChange={(v) =>
($inputs.allowUserSignups.value = v as typeof $inputs.allowUserSignups.value)}
>
<Select.Trigger
class="w-full"
aria-label={m.enable_user_signups()}
placeholder={m.enable_user_signups()}
>
{signupOptions[$inputs.allowUserSignups.value]?.label}
</Select.Trigger>
<Select.Content>
<Select.Item value="disabled">
<div class="flex flex-col items-start gap-1">
<span class="font-medium">{signupOptions.disabled.label}</span>
<span class="text-muted-foreground text-xs">
{signupOptions.disabled.description}
</span>
</div>
</Select.Item>
<Select.Item value="withToken">
<div class="flex flex-col items-start gap-1">
<span class="font-medium">{signupOptions.withToken.label}</span>
<span class="text-muted-foreground text-xs">
{signupOptions.withToken.description}
</span>
</div>
</Select.Item>
<Select.Item value="open">
<div class="flex flex-col items-start gap-1">
<span class="font-medium">{signupOptions.open.label}</span>
<span class="text-muted-foreground text-xs">
{signupOptions.open.description}
</span>
</div>
</Select.Item>
</Select.Content>
</Select.Root>
</div>
<SwitchWithLabel <SwitchWithLabel
id="self-account-editing" id="self-account-editing"
label={m.enable_self_account_editing()} label={m.enable_self_account_editing()}

View File

@@ -0,0 +1,176 @@
<script lang="ts">
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
import SearchableMultiSelect from '$lib/components/form/searchable-multi-select.svelte';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { m } from '$lib/paraglide/messages';
import UserGroupService from '$lib/services/user-group-service';
import type { AllAppConfig } from '$lib/types/application-configuration';
import { debounced } from '$lib/utils/debounce-util';
import { preventDefault } from '$lib/utils/event-util';
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
let {
appConfig,
callback
}: {
appConfig: AllAppConfig;
callback: (updatedConfig: Partial<AllAppConfig>) => Promise<void>;
} = $props();
const userGroupService = new UserGroupService();
let userGroups = $state<{ value: string; label: string }[]>([]);
let selectedGroups = $state<{ value: string; label: string }[]>([]);
let customClaims = $state(appConfig.signupDefaultCustomClaims || []);
let allowUserSignups = $state(appConfig.allowUserSignups);
let isLoading = $state(false);
let isUserSearchLoading = $state(false);
const signupOptions = {
disabled: {
label: m.disabled(),
description: m.signup_disabled_description()
},
withToken: {
label: m.signup_with_token(),
description: m.signup_with_token_description()
},
open: {
label: m.signup_open(),
description: m.signup_open_description()
}
};
async function loadUserGroups(search?: string) {
userGroups = (await userGroupService.list({ search })).data.map((group) => ({
value: group.id,
label: group.name
}));
// Ensure selected groups are still in the list
for (const selectedGroup of selectedGroups) {
if (!userGroups.some((g) => g.value === selectedGroup.value)) {
userGroups.push(selectedGroup);
}
}
}
async function loadSelectedGroups() {
selectedGroups = (
await Promise.all(
appConfig.signupDefaultUserGroupIDs.map((groupId) => userGroupService.get(groupId))
)
).map((group) => ({
value: group.id,
label: group.name
}));
}
const onUserGroupSearch = debounced(
async (search: string) => await loadUserGroups(search),
300,
(loading) => (isUserSearchLoading = loading)
);
async function onSubmit() {
isLoading = true;
await callback({
allowUserSignups: allowUserSignups,
signupDefaultUserGroupIDs: selectedGroups.map((g) => g.value),
signupDefaultCustomClaims: customClaims
});
toast.success(m.user_creation_updated_successfully());
isLoading = false;
}
$effect(() => {
loadSelectedGroups();
customClaims = appConfig.signupDefaultCustomClaims || [];
allowUserSignups = appConfig.allowUserSignups;
});
onMount(() => loadUserGroups());
</script>
<form class="space-y-6" onsubmit={preventDefault(onSubmit)}>
<div class="grid gap-2">
<div>
<Label class="mb-0" for="enable-user-signup">{m.enable_user_signups()}</Label>
<p class="text-muted-foreground text-[0.8rem]">
{m.enable_user_signups_description()}
</p>
</div>
<Select.Root
type="single"
value={allowUserSignups}
onValueChange={(v) => (allowUserSignups = v as typeof allowUserSignups)}
>
<Select.Trigger
id="enable-user-signup"
class="w-full"
aria-label={m.enable_user_signups()}
placeholder={m.enable_user_signups()}
>
{signupOptions[allowUserSignups]?.label}
</Select.Trigger>
<Select.Content>
<Select.Item value="disabled">
<div class="flex flex-col items-start gap-1">
<span class="font-medium">{signupOptions.disabled.label}</span>
<span class="text-muted-foreground text-xs">
{signupOptions.disabled.description}
</span>
</div>
</Select.Item>
<Select.Item value="withToken">
<div class="flex flex-col items-start gap-1">
<span class="font-medium">{signupOptions.withToken.label}</span>
<span class="text-muted-foreground text-xs">
{signupOptions.withToken.description}
</span>
</div>
</Select.Item>
<Select.Item value="open">
<div class="flex flex-col items-start gap-1">
<span class="font-medium">{signupOptions.open.label}</span>
<span class="text-muted-foreground text-xs">
{signupOptions.open.description}
</span>
</div>
</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div>
<Label for="default-groups" class="mb-0">{m.user_groups()}</Label>
<p class="text-muted-foreground mt-1 mb-2 text-xs">
{m.user_creation_groups_description()}
</p>
<SearchableMultiSelect
id="default-groups"
items={userGroups}
oninput={(e) => onUserGroupSearch(e.currentTarget.value)}
selectedItems={selectedGroups.map((g) => g.value)}
onSelect={(selected) => {
selectedGroups = userGroups.filter((g) => selected.includes(g.value));
}}
isLoading={isUserSearchLoading}
disableInternalSearch
/>
</div>
<div>
<Label class="mb-0">{m.custom_claims()}</Label>
<p class="text-muted-foreground mt-1 mb-2 text-xs">
{m.user_creation_claims_description()}
</p>
<CustomClaimsInput bind:customClaims />
</div>
<div class="flex justify-end pt-2">
<Button {isLoading} type="submit">{m.save()}</Button>
</div>
</form>

View File

@@ -70,7 +70,7 @@
{#if expandAddClient} {#if expandAddClient}
<div transition:slide> <div transition:slide>
<Card.Content> <Card.Content>
<OIDCClientForm callback={createOIDCClient} /> <OIDCClientForm mode="create" callback={createOIDCClient} />
</Card.Content> </Card.Content>
</div> </div>
{/if} {/if}

View File

@@ -36,7 +36,8 @@
[m.userinfo_url()]: `https://${page.url.hostname}/api/oidc/userinfo`, [m.userinfo_url()]: `https://${page.url.hostname}/api/oidc/userinfo`,
[m.logout_url()]: `https://${page.url.hostname}/api/oidc/end-session`, [m.logout_url()]: `https://${page.url.hostname}/api/oidc/end-session`,
[m.certificate_url()]: `https://${page.url.hostname}/.well-known/jwks.json`, [m.certificate_url()]: `https://${page.url.hostname}/.well-known/jwks.json`,
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled() [m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled(),
[m.requires_reauthentication()]: client.requiresReauthentication ? m.enabled() : m.disabled()
}); });
async function updateClient(updatedClient: OidcClientCreateWithLogo) { async function updateClient(updatedClient: OidcClientCreateWithLogo) {
@@ -49,6 +50,9 @@
client.isPublic = updatedClient.isPublic; client.isPublic = updatedClient.isPublic;
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled(); setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
setupDetails[m.requires_reauthentication()] = updatedClient.requiresReauthentication
? m.enabled()
: m.disabled();
await Promise.all([dataPromise, imagePromise]) await Promise.all([dataPromise, imagePromise])
.then(() => { .then(() => {
@@ -120,14 +124,14 @@
<Card.Content> <Card.Content>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="mb-2 flex flex-col sm:flex-row sm:items-center"> <div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">{m.client_id()}</Label> <Label class="mb-0 w-50">{m.client_id()}</Label>
<CopyToClipboard value={client.id}> <CopyToClipboard value={client.id}>
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span> <span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
</CopyToClipboard> </CopyToClipboard>
</div> </div>
{#if !client.isPublic} {#if !client.isPublic}
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center"> <div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">{m.client_secret()}</Label> <Label class="mb-0 w-50">{m.client_secret()}</Label>
{#if $clientSecretStore} {#if $clientSecretStore}
<CopyToClipboard value={$clientSecretStore}> <CopyToClipboard value={$clientSecretStore}>
<span class="text-muted-foreground text-sm" data-testid="client-secret"> <span class="text-muted-foreground text-sm" data-testid="client-secret">
@@ -154,7 +158,7 @@
<div transition:slide> <div transition:slide>
{#each Object.entries(setupDetails) as [key, value]} {#each Object.entries(setupDetails) as [key, value]}
<div class="mb-5 flex flex-col sm:flex-row sm:items-center"> <div class="mb-5 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">{key}</Label> <Label class="mb-0 w-50">{key}</Label>
<CopyToClipboard {value}> <CopyToClipboard {value}>
<span class="text-muted-foreground text-sm">{value}</span> <span class="text-muted-foreground text-sm">{value}</span>
</CopyToClipboard> </CopyToClipboard>
@@ -175,7 +179,7 @@
</Card.Root> </Card.Root>
<Card.Root> <Card.Root>
<Card.Content> <Card.Content>
<OidcForm existingClient={client} callback={updateClient} /> <OidcForm mode="update" existingClient={client} callback={updateClient} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
<CollapsibleCard <CollapsibleCard

View File

@@ -6,24 +6,30 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Label from '$lib/components/ui/label/label.svelte'; import Label from '$lib/components/ui/label/label.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import type { OidcClient, OidcClientCreateWithLogo } from '$lib/types/oidc.type'; import type {
OidcClient,
OidcClientCreateWithLogo,
OidcClientUpdateWithLogo
} from '$lib/types/oidc.type';
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util'; import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import { preventDefault } from '$lib/utils/event-util'; import { preventDefault } from '$lib/utils/event-util';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
import { cn } from '$lib/utils/style'; import { cn } from '$lib/utils/style';
import { emptyToUndefined, optionalUrl } from '$lib/utils/zod-util';
import { LucideChevronDown } from '@lucide/svelte'; import { LucideChevronDown } from '@lucide/svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { z } from 'zod/v4'; import { z } from 'zod/v4';
import FederatedIdentitiesInput from './federated-identities-input.svelte'; import FederatedIdentitiesInput from './federated-identities-input.svelte';
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte'; import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
import { optionalUrl } from '$lib/utils/zod-util';
let { let {
callback, callback,
existingClient existingClient,
mode
}: { }: {
existingClient?: OidcClient; existingClient?: OidcClient;
callback: (user: OidcClientCreateWithLogo) => Promise<boolean>; callback: (client: OidcClientCreateWithLogo | OidcClientUpdateWithLogo) => Promise<boolean>;
mode: 'create' | 'update';
} = $props(); } = $props();
let isLoading = $state(false); let isLoading = $state(false);
@@ -34,11 +40,13 @@
); );
const client = { const client = {
id: '',
name: existingClient?.name || '', name: existingClient?.name || '',
callbackURLs: existingClient?.callbackURLs || [], callbackURLs: existingClient?.callbackURLs || [],
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [], logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
isPublic: existingClient?.isPublic || false, isPublic: existingClient?.isPublic || false,
pkceEnabled: existingClient?.pkceEnabled || false, pkceEnabled: existingClient?.pkceEnabled || false,
requiresReauthentication: existingClient?.requiresReauthentication || false,
launchURL: existingClient?.launchURL || '', launchURL: existingClient?.launchURL || '',
credentials: { credentials: {
federatedIdentities: existingClient?.credentials?.federatedIdentities || [] federatedIdentities: existingClient?.credentials?.federatedIdentities || []
@@ -46,11 +54,22 @@
}; };
const formSchema = z.object({ const formSchema = z.object({
id: emptyToUndefined(
z
.string()
.min(2)
.max(128)
.regex(/^[a-zA-Z0-9_-]+$/, {
message: m.invalid_client_id()
})
.optional()
),
name: z.string().min(2).max(50), name: z.string().min(2).max(50),
callbackURLs: z.array(z.string().nonempty()).default([]), callbackURLs: z.array(z.string().nonempty()).default([]),
logoutCallbackURLs: z.array(z.string().nonempty()), logoutCallbackURLs: z.array(z.string().nonempty()),
isPublic: z.boolean(), isPublic: z.boolean(),
pkceEnabled: z.boolean(), pkceEnabled: z.boolean(),
requiresReauthentication: z.boolean(),
launchURL: optionalUrl, launchURL: optionalUrl,
credentials: z.object({ credentials: z.object({
federatedIdentities: z.array( federatedIdentities: z.array(
@@ -147,6 +166,12 @@
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()} description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
bind:checked={$inputs.pkceEnabled.value} bind:checked={$inputs.pkceEnabled.value}
/> />
<SwitchWithLabel
id="requires-reauthentication"
label={m.requires_reauthentication()}
description={m.requires_users_to_authenticate_again_on_each_authorization()}
bind:checked={$inputs.requiresReauthentication.value}
/>
</div> </div>
<div class="mt-8"> <div class="mt-8">
<Label for="logo">{m.logo()}</Label> <Label for="logo">{m.logo()}</Label>
@@ -177,7 +202,16 @@
</div> </div>
{#if showAdvancedOptions} {#if showAdvancedOptions}
<div class="mt-5 md:col-span-2" transition:slide={{ duration: 200 }}> <div class="mt-7 flex flex-col gap-y-7 md:col-span-2" transition:slide={{ duration: 200 }}>
{#if mode == 'create'}
<FormInput
label={m.client_id()}
placeholder={m.generated()}
class="w-full md:w-1/2"
description={m.custom_client_id_description()}
bind:input={$inputs.id}
/>
{/if}
<FederatedIdentitiesInput <FederatedIdentitiesInput
client={existingClient} client={existingClient}
bind:federatedIdentities={$inputs.credentials.value.federatedIdentities} bind:federatedIdentities={$inputs.credentials.value.federatedIdentities}
@@ -189,7 +223,7 @@
<div class="relative mt-5 flex justify-center"> <div class="relative mt-5 flex justify-center">
<Button <Button
variant="ghost" variant="ghost"
class="text-muted-foregroun" class="text-muted-foreground"
onclick={() => (showAdvancedOptions = !showAdvancedOptions)} onclick={() => (showAdvancedOptions = !showAdvancedOptions)}
> >
{showAdvancedOptions ? m.hide_advanced_options() : m.show_advanced_options()} {showAdvancedOptions ? m.hide_advanced_options() : m.show_advanced_options()}

View File

@@ -3,25 +3,25 @@
import * as Pagination from '$lib/components/ui/pagination'; import * as Pagination from '$lib/components/ui/pagination';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import OIDCService from '$lib/services/oidc-service'; import OIDCService from '$lib/services/oidc-service';
import type { AuthorizedOidcClient, OidcClientMetaData } from '$lib/types/oidc.type'; import type { AccessibleOidcClient, OidcClientMetaData } from '$lib/types/oidc.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { LayoutDashboard } from '@lucide/svelte'; import { LayoutDashboard } from '@lucide/svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { default as AuthorizedOidcClientCard } from './authorized-oidc-client-card.svelte'; import AuthorizedOidcClientCard from './authorized-oidc-client-card.svelte';
let { data } = $props(); let { data } = $props();
let authorizedClients: Paginated<AuthorizedOidcClient> = $state(data.authorizedClients); let clients: Paginated<AccessibleOidcClient> = $state(data.clients);
let requestOptions: SearchPaginationSortRequest = $state(data.appRequestOptions); let requestOptions: SearchPaginationSortRequest = $state(data.appRequestOptions);
const oidcService = new OIDCService(); const oidcService = new OIDCService();
async function onRefresh(options: SearchPaginationSortRequest) { async function onRefresh(options: SearchPaginationSortRequest) {
authorizedClients = await oidcService.listAuthorizedClients(options); clients = await oidcService.listOwnAccessibleClients(options);
} }
async function onPageChange(page: number) { async function onPageChange(page: number) {
requestOptions.pagination = { limit: authorizedClients.pagination.itemsPerPage, page }; requestOptions.pagination = { limit: clients.pagination.itemsPerPage, page };
onRefresh(requestOptions); onRefresh(requestOptions);
} }
@@ -64,7 +64,7 @@
</h1> </h1>
</div> </div>
{#if authorizedClients.data.length === 0} {#if clients.data.length === 0}
<div class="py-16 text-center"> <div class="py-16 text-center">
<LayoutDashboard class="text-muted-foreground mx-auto mb-4 size-16" /> <LayoutDashboard class="text-muted-foreground mx-auto mb-4 size-16" />
<h3 class="text-muted-foreground mb-2 text-lg font-medium"> <h3 class="text-muted-foreground mb-2 text-lg font-medium">
@@ -76,20 +76,23 @@
</div> </div>
{:else} {:else}
<div class="space-y-8"> <div class="space-y-8">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4"> <div
{#each authorizedClients.data as authorizedClient} class="grid gap-3"
<AuthorizedOidcClientCard {authorizedClient} onRevoke={revokeAuthorizedClient} /> style="grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));"
>
{#each clients.data as client}
<AuthorizedOidcClientCard {client} onRevoke={revokeAuthorizedClient} />
{/each} {/each}
</div> </div>
{#if authorizedClients.pagination.totalPages > 1} {#if clients.pagination.totalPages > 1}
<div class="border-border flex items-center justify-center border-t pt-3"> <div class="border-border flex items-center justify-center border-t pt-3">
<Pagination.Root <Pagination.Root
class="mx-0 w-auto" class="mx-0 w-auto"
count={authorizedClients.pagination.totalItems} count={clients.pagination.totalItems}
perPage={authorizedClients.pagination.itemsPerPage} perPage={clients.pagination.itemsPerPage}
{onPageChange} {onPageChange}
page={authorizedClients.pagination.currentPage} page={clients.pagination.currentPage}
> >
{#snippet children({ pages })} {#snippet children({ pages })}
<Pagination.Content class="flex justify-center"> <Pagination.Content class="flex justify-center">
@@ -101,7 +104,7 @@
<Pagination.Item> <Pagination.Item>
<Pagination.Link <Pagination.Link
{page} {page}
isActive={authorizedClients.pagination.currentPage === page.value} isActive={clients.pagination.currentPage === page.value}
> >
{page.value} {page.value}
</Pagination.Link> </Pagination.Link>

View File

@@ -16,7 +16,7 @@ export const load: PageLoad = async () => {
} }
}; };
const authorizedClients = await oidcService.listAuthorizedClients(appRequestOptions); const clients = await oidcService.listOwnAccessibleClients(appRequestOptions);
return { authorizedClients, appRequestOptions }; return { clients, appRequestOptions };
}; };

View File

@@ -4,23 +4,26 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import type { AuthorizedOidcClient, OidcClientMetaData } from '$lib/types/oidc.type'; import type { AccessibleOidcClient, OidcClientMetaData } from '$lib/types/oidc.type';
import { cachedApplicationLogo, cachedOidcClientLogo } from '$lib/utils/cached-image-util'; import { cachedApplicationLogo, cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import { import {
LucideBan, LucideBan,
LucideEllipsisVertical, LucideEllipsisVertical,
LucideExternalLink, LucideExternalLink,
LucideLogIn,
LucidePencil LucidePencil
} from '@lucide/svelte'; } from '@lucide/svelte';
import { formatDistanceToNow } from 'date-fns';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
let { let {
authorizedClient, client,
onRevoke onRevoke
}: { }: {
authorizedClient: AuthorizedOidcClient; client: AccessibleOidcClient;
onRevoke: (client: OidcClientMetaData) => Promise<void>; onRevoke: (client: OidcClientMetaData) => Promise<void>;
} = $props(); } = $props();
@@ -28,7 +31,7 @@
</script> </script>
<Card.Root <Card.Root
class="border-muted group h-[140px] p-5 transition-all duration-200 hover:shadow-md" class="border-muted group relative h-[140px] p-5 transition-all duration-200 hover:shadow-md"
data-testid="authorized-oidc-client-card" data-testid="authorized-oidc-client-card"
> >
<Card.Content class=" p-0"> <Card.Content class=" p-0">
@@ -36,60 +39,84 @@
<div class="aspect-square h-[56px]"> <div class="aspect-square h-[56px]">
<ImageBox <ImageBox
class="grow rounded-lg object-contain" class="grow rounded-lg object-contain"
src={authorizedClient.client.hasLogo src={client.hasLogo
? cachedOidcClientLogo.getUrl(authorizedClient.client.id) ? cachedOidcClientLogo.getUrl(client.id)
: cachedApplicationLogo.getUrl(isLightMode)} : cachedApplicationLogo.getUrl(isLightMode)}
alt={m.name_logo({ name: authorizedClient.client.name })} alt={m.name_logo({ name: client.name })}
/> />
</div> </div>
<div class="flex w-full justify-between gap-3"> <div class="flex w-full justify-between gap-3">
<div> <div>
<div class="mb-1 flex items-start gap-2"> <div class="mb-1 flex items-start gap-2">
<h3 <h3
class="text-foreground line-clamp-2 leading-tight font-semibold break-words break-all text-ellipsis" class="text-foreground line-clamp-2 text-ellipsis break-words break-all font-semibold leading-tight"
> >
{authorizedClient.client.name} {client.name}
</h3> </h3>
</div> </div>
{#if authorizedClient.client.launchURL} {#if client.launchURL}
<p <p
class="text-muted-foreground line-clamp-1 text-xs break-words break-all text-ellipsis" class="text-muted-foreground line-clamp-1 text-ellipsis break-words break-all text-xs"
> >
{new URL(authorizedClient.client.launchURL).hostname} {new URL(client.launchURL).hostname}
</p> </p>
{/if} {/if}
</div> </div>
<div> {#if $userStore?.isAdmin || client.lastUsedAt}
<DropdownMenu.Root> <div>
<DropdownMenu.Trigger> <DropdownMenu.Root>
<LucideEllipsisVertical class="size-4" /> <DropdownMenu.Trigger>
<span class="sr-only">{m.toggle_menu()}</span> <LucideEllipsisVertical class="size-4" />
</DropdownMenu.Trigger> <span class="sr-only">{m.toggle_menu()}</span>
<DropdownMenu.Content align="end"> </DropdownMenu.Trigger>
<DropdownMenu.Item <DropdownMenu.Content align="end">
onclick={() => goto(`/settings/admin/oidc-clients/${authorizedClient.client.id}`)} {#if $userStore?.isAdmin}
><LucidePencil class="mr-2 size-4" /> {m.edit()}</DropdownMenu.Item <DropdownMenu.Item
> onclick={() => goto(`/settings/admin/oidc-clients/${client.id}`)}
{#if $userStore?.isAdmin} ><LucidePencil class="mr-2 size-4" /> {m.edit()}</DropdownMenu.Item
<DropdownMenu.Item >
class="text-red-500 focus:!text-red-700" {/if}
onclick={() => onRevoke(authorizedClient.client)} {#if client.lastUsedAt}
><LucideBan class="mr-2 size-4" />{m.revoke()}</DropdownMenu.Item <DropdownMenu.Item
> class="text-red-500 focus:!text-red-700"
{/if} onclick={() => onRevoke(client)}
</DropdownMenu.Content> ><LucideBan class="mr-2 size-4" />{m.revoke()}</DropdownMenu.Item
</DropdownMenu.Root> >
</div> {/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/if}
</div> </div>
</div> </div>
<div class="mt-2 flex justify-end"> <div class="mt-2 flex items-end justify-between">
{#if client.lastUsedAt}
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<p class="text-muted-foreground flex items-center text-xs">
<LucideLogIn class="mr-1 size-3" />
{formatDistanceToNow(client.lastUsedAt, { addSuffix: true })}
</p>
</Tooltip.Trigger>
<Tooltip.Content
>{m.last_signed_in_ago({
time: formatDistanceToNow(client.lastUsedAt)
})}</Tooltip.Content
>
</Tooltip.Root></Tooltip.Provider
>
{:else}
<div></div>
{/if}
<Button <Button
href={authorizedClient.client.launchURL} href={client.launchURL}
target="_blank" target="_blank"
size="sm" size="sm"
class="h-8 text-xs" class="h-8 text-xs"
disabled={!authorizedClient.client.launchURL} rel="noopener noreferrer"
disabled={!client.launchURL}
> >
{m.launch()} {m.launch()}
<LucideExternalLink class="ml-1 size-3" /> <LucideExternalLink class="ml-1 size-3" />

View File

@@ -32,7 +32,7 @@
return false; return false;
} }
userStore.setUser(result.data); await userStore.setUser(result.data);
isLoading = false; isLoading = false;
goto('/signup/add-passkey'); goto('/signup/add-passkey');

View File

@@ -30,7 +30,7 @@
return false; return false;
} }
userStore.setUser(result.data); await userStore.setUser(result.data);
isLoading = false; isLoading = false;
goto('/signup/add-passkey'); goto('/signup/add-passkey');

Binary file not shown.

179
pnpm-lock.yaml generated
View File

@@ -22,6 +22,9 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
jose: jose:
specifier: ^5.10.0 specifier: ^5.10.0
version: 5.10.0 version: 5.10.0
@@ -101,6 +104,9 @@ importers:
prettier-plugin-tailwindcss: prettier-plugin-tailwindcss:
specifier: ^0.6.14 specifier: ^0.6.14
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.36.17))(prettier@3.6.2) version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.36.17))(prettier@3.6.2)
rollup:
specifier: ^4.46.3
version: 4.46.3
svelte: svelte:
specifier: ^5.36.16 specifier: ^5.36.16
version: 5.36.17 version: 5.36.17
@@ -474,103 +480,103 @@ packages:
'@poppinss/macroable@1.0.5': '@poppinss/macroable@1.0.5':
resolution: {integrity: sha512-6u61y1HHd090MEk1Av0/1btDmm2Hh/+XoJj+HgFYRh9koUPI822ybJbwLHuqjLNCiY+o1gRykg2igEqOf/VBZw==} resolution: {integrity: sha512-6u61y1HHd090MEk1Av0/1btDmm2Hh/+XoJj+HgFYRh9koUPI822ybJbwLHuqjLNCiY+o1gRykg2igEqOf/VBZw==}
'@rollup/rollup-android-arm-eabi@4.45.3': '@rollup/rollup-android-arm-eabi@4.46.3':
resolution: {integrity: sha512-8oQkCTve4H4B4JpmD2FV7fV2ZPTxJHN//bRhCqPUU8v6c5APlxteAXyc7BFaEb4aGpUzrPLU4PoAcGhwmRzZTA==} resolution: {integrity: sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
'@rollup/rollup-android-arm64@4.45.3': '@rollup/rollup-android-arm64@4.46.3':
resolution: {integrity: sha512-StOsmdXHU2hx3UFTTs6yYxCSwSIgLsfjUBICXyWj625M32OOjakXlaZuGKL+jA3Nvv35+hMxrm/64eCoT07SYQ==} resolution: {integrity: sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@rollup/rollup-darwin-arm64@4.45.3': '@rollup/rollup-darwin-arm64@4.46.3':
resolution: {integrity: sha512-6CfLF3eqKhCdhK0GUnR5ZS99OFz+dtOeB/uePznLKxjCsk5QjT/V0eSEBb4vj+o/ri3i35MseSEQHCLLAgClVw==} resolution: {integrity: sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@rollup/rollup-darwin-x64@4.45.3': '@rollup/rollup-darwin-x64@4.46.3':
resolution: {integrity: sha512-QLWyWmAJG9elNTNLdcSXUT/M+J7DhEmvs1XPHYcgYkse3UHf9iWTJ+yTPlKMIetiQnNi+cNp+gY4gvjDpREfKw==} resolution: {integrity: sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@rollup/rollup-freebsd-arm64@4.45.3': '@rollup/rollup-freebsd-arm64@4.46.3':
resolution: {integrity: sha512-ZOvBq+5nL0yrZIEo1eq6r7MPvkJ8kC1XATS/yHvcq3WbDNKNKBQ1uIF4hicyzDMoJt72G+sn1nKsFXpifZyRDA==} resolution: {integrity: sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
'@rollup/rollup-freebsd-x64@4.45.3': '@rollup/rollup-freebsd-x64@4.46.3':
resolution: {integrity: sha512-AYvGR07wecEnyYSovyJ71pTOulbNvsrpRpK6i/IM1b0UGX1vFx51afYuPYPxnvE9aCl5xPnhQicEvdIMxClRgQ==} resolution: {integrity: sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.45.3': '@rollup/rollup-linux-arm-gnueabihf@4.46.3':
resolution: {integrity: sha512-Yx8Cp38tfRRToVLuIWzBHV25/QPzpUreOPIiUuNV7KahNPurYg2pYQ4l7aYnvpvklO1riX4643bXLvDsYSBIrA==} resolution: {integrity: sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.45.3': '@rollup/rollup-linux-arm-musleabihf@4.46.3':
resolution: {integrity: sha512-4dIYRNxlXGDKnO6qgcda6LxnObPO6r1OBU9HG8F9pAnHHLtfbiOqCzDvkeHknx+5mfFVH4tWOl+h+cHylwsPWA==} resolution: {integrity: sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.45.3': '@rollup/rollup-linux-arm64-gnu@4.46.3':
resolution: {integrity: sha512-M6uVlWKmhLN7LguLDu6396K1W5IBlAaRonjlHQgc3s4dOGceu0FeBuvbXiUPYvup/6b5Ln7IEX7XNm68DN4vrg==} resolution: {integrity: sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@rollup/rollup-linux-arm64-musl@4.45.3': '@rollup/rollup-linux-arm64-musl@4.46.3':
resolution: {integrity: sha512-emaYiOTQJUd6fC9a6jcw9zIWtzaUiuBC+vomggaM4In2iOra/lA6IMHlqZqQZr08NYXrOPMVigreLMeSAwv3Uw==} resolution: {integrity: sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@rollup/rollup-linux-loongarch64-gnu@4.45.3': '@rollup/rollup-linux-loongarch64-gnu@4.46.3':
resolution: {integrity: sha512-3P77T5AQ4UfVRJSrTKLiUZDJ6XsxeP80027bp6mOFh8sevSD038mYuIYFiUtrSJxxgFb+NgRJFF9oIa0rlUsmg==} resolution: {integrity: sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.45.3': '@rollup/rollup-linux-ppc64-gnu@4.46.3':
resolution: {integrity: sha512-/VPH3ZVeSlmCBPhZdx/+4dMXDjaGMhDsWOBo9EwSkGbw2+OAqaslL53Ao2OqCxR0GgYjmmssJ+OoG+qYGE7IBg==} resolution: {integrity: sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.45.3': '@rollup/rollup-linux-riscv64-gnu@4.46.3':
resolution: {integrity: sha512-Hs5if0PjROl1MGMmZX3xMAIfqcGxQE2SJWUr/CpDQsOQn43Wq4IvXXxUMWtiY/BrzdqCCJlRgJ5DKxzS3qWkCw==} resolution: {integrity: sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.45.3': '@rollup/rollup-linux-riscv64-musl@4.46.3':
resolution: {integrity: sha512-Qm0WOwh3Lk388+HJFl1ILGbd2iOoQf6yl4fdGqOjBzEA+5JYbLcwd+sGsZjs5pkt8Cr/1G42EiXmlRp9ZeTvFA==} resolution: {integrity: sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.45.3': '@rollup/rollup-linux-s390x-gnu@4.46.3':
resolution: {integrity: sha512-VJdknTaYw+TqXzlh9c7vaVMh/fV2sU8Khfk4a9vAdYXJawpjf6z3U1k7vDWx2IQ9ZOPoOPxgVpDfYOYhxD7QUA==} resolution: {integrity: sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@rollup/rollup-linux-x64-gnu@4.45.3': '@rollup/rollup-linux-x64-gnu@4.46.3':
resolution: {integrity: sha512-SUDXU5YabLAMl86FpupSQQEWzVG8X0HM+Q/famnJusbPiUgQnTGuSxtxg4UAYgv1ZmRV1nioYYXsgtSokU/7+Q==} resolution: {integrity: sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@rollup/rollup-linux-x64-musl@4.45.3': '@rollup/rollup-linux-x64-musl@4.46.3':
resolution: {integrity: sha512-ezmqknOUFgZMN6wW+Avlo4sXF3Frswd+ncrwMz4duyZ5Eqd+dAYgJ+A1MY+12LNZ7XDhCiijJceueYvtnzdviw==} resolution: {integrity: sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.45.3': '@rollup/rollup-win32-arm64-msvc@4.46.3':
resolution: {integrity: sha512-1YfXoUEE++gIW66zNB9Twd0Ua5xCXpfYppFUxVT/Io5ZT3fO6Se+C/Jvmh3usaIHHyi53t3kpfjydO2GAy5eBA==} resolution: {integrity: sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.45.3': '@rollup/rollup-win32-ia32-msvc@4.46.3':
resolution: {integrity: sha512-Iok2YA3PvC163rVZf2Zy81A0g88IUcSPeU5pOilcbICXre2EP1mxn1Db/l09Z/SK1vdSLtpJXAnwGuMOyf5O9g==} resolution: {integrity: sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@rollup/rollup-win32-x64-msvc@4.45.3': '@rollup/rollup-win32-x64-msvc@4.46.3':
resolution: {integrity: sha512-HwHCH5GQTOeGYP5wBEBXFVhfQecwRl24Rugoqhh8YwGarsU09bHhOKuqlyW4ZolZCan3eTUax7UJbGSmKSM51A==} resolution: {integrity: sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -977,6 +983,9 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
hasBin: true hasBin: true
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.13: dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@@ -1763,8 +1772,8 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rollup@4.45.3: rollup@4.46.3:
resolution: {integrity: sha512-STwyHZF3G+CrmZhB+qDiROq9s8B5PrOCYN6dtmOvwz585XBnyeHk1GTEhHJtUVb355/9uZhOazyVclTt5uahzA==} resolution: {integrity: sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
@@ -2423,64 +2432,64 @@ snapshots:
'@poppinss/macroable@1.0.5': '@poppinss/macroable@1.0.5':
optional: true optional: true
'@rollup/rollup-android-arm-eabi@4.45.3': '@rollup/rollup-android-arm-eabi@4.46.3':
optional: true optional: true
'@rollup/rollup-android-arm64@4.45.3': '@rollup/rollup-android-arm64@4.46.3':
optional: true optional: true
'@rollup/rollup-darwin-arm64@4.45.3': '@rollup/rollup-darwin-arm64@4.46.3':
optional: true optional: true
'@rollup/rollup-darwin-x64@4.45.3': '@rollup/rollup-darwin-x64@4.46.3':
optional: true optional: true
'@rollup/rollup-freebsd-arm64@4.45.3': '@rollup/rollup-freebsd-arm64@4.46.3':
optional: true optional: true
'@rollup/rollup-freebsd-x64@4.45.3': '@rollup/rollup-freebsd-x64@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.45.3': '@rollup/rollup-linux-arm-gnueabihf@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-arm-musleabihf@4.45.3': '@rollup/rollup-linux-arm-musleabihf@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-arm64-gnu@4.45.3': '@rollup/rollup-linux-arm64-gnu@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-arm64-musl@4.45.3': '@rollup/rollup-linux-arm64-musl@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-loongarch64-gnu@4.45.3': '@rollup/rollup-linux-loongarch64-gnu@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-ppc64-gnu@4.45.3': '@rollup/rollup-linux-ppc64-gnu@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-riscv64-gnu@4.45.3': '@rollup/rollup-linux-riscv64-gnu@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-riscv64-musl@4.45.3': '@rollup/rollup-linux-riscv64-musl@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-s390x-gnu@4.45.3': '@rollup/rollup-linux-s390x-gnu@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-x64-gnu@4.45.3': '@rollup/rollup-linux-x64-gnu@4.46.3':
optional: true optional: true
'@rollup/rollup-linux-x64-musl@4.45.3': '@rollup/rollup-linux-x64-musl@4.46.3':
optional: true optional: true
'@rollup/rollup-win32-arm64-msvc@4.45.3': '@rollup/rollup-win32-arm64-msvc@4.46.3':
optional: true optional: true
'@rollup/rollup-win32-ia32-msvc@4.45.3': '@rollup/rollup-win32-ia32-msvc@4.46.3':
optional: true optional: true
'@rollup/rollup-win32-x64-msvc@4.45.3': '@rollup/rollup-win32-x64-msvc@4.46.3':
optional: true optional: true
'@sideway/address@4.1.5': '@sideway/address@4.1.5':
@@ -2919,6 +2928,8 @@ snapshots:
cssesc@3.0.0: {} cssesc@3.0.0: {}
date-fns@4.1.0: {}
dayjs@1.11.13: dayjs@1.11.13:
optional: true optional: true
@@ -3569,30 +3580,30 @@ snapshots:
reusify@1.1.0: {} reusify@1.1.0: {}
rollup@4.45.3: rollup@4.46.3:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
optionalDependencies: optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.45.3 '@rollup/rollup-android-arm-eabi': 4.46.3
'@rollup/rollup-android-arm64': 4.45.3 '@rollup/rollup-android-arm64': 4.46.3
'@rollup/rollup-darwin-arm64': 4.45.3 '@rollup/rollup-darwin-arm64': 4.46.3
'@rollup/rollup-darwin-x64': 4.45.3 '@rollup/rollup-darwin-x64': 4.46.3
'@rollup/rollup-freebsd-arm64': 4.45.3 '@rollup/rollup-freebsd-arm64': 4.46.3
'@rollup/rollup-freebsd-x64': 4.45.3 '@rollup/rollup-freebsd-x64': 4.46.3
'@rollup/rollup-linux-arm-gnueabihf': 4.45.3 '@rollup/rollup-linux-arm-gnueabihf': 4.46.3
'@rollup/rollup-linux-arm-musleabihf': 4.45.3 '@rollup/rollup-linux-arm-musleabihf': 4.46.3
'@rollup/rollup-linux-arm64-gnu': 4.45.3 '@rollup/rollup-linux-arm64-gnu': 4.46.3
'@rollup/rollup-linux-arm64-musl': 4.45.3 '@rollup/rollup-linux-arm64-musl': 4.46.3
'@rollup/rollup-linux-loongarch64-gnu': 4.45.3 '@rollup/rollup-linux-loongarch64-gnu': 4.46.3
'@rollup/rollup-linux-ppc64-gnu': 4.45.3 '@rollup/rollup-linux-ppc64-gnu': 4.46.3
'@rollup/rollup-linux-riscv64-gnu': 4.45.3 '@rollup/rollup-linux-riscv64-gnu': 4.46.3
'@rollup/rollup-linux-riscv64-musl': 4.45.3 '@rollup/rollup-linux-riscv64-musl': 4.46.3
'@rollup/rollup-linux-s390x-gnu': 4.45.3 '@rollup/rollup-linux-s390x-gnu': 4.46.3
'@rollup/rollup-linux-x64-gnu': 4.45.3 '@rollup/rollup-linux-x64-gnu': 4.46.3
'@rollup/rollup-linux-x64-musl': 4.45.3 '@rollup/rollup-linux-x64-musl': 4.46.3
'@rollup/rollup-win32-arm64-msvc': 4.45.3 '@rollup/rollup-win32-arm64-msvc': 4.46.3
'@rollup/rollup-win32-ia32-msvc': 4.45.3 '@rollup/rollup-win32-ia32-msvc': 4.46.3
'@rollup/rollup-win32-x64-msvc': 4.45.3 '@rollup/rollup-win32-x64-msvc': 4.46.3
fsevents: 2.3.3 fsevents: 2.3.3
run-parallel@1.2.0: run-parallel@1.2.0:
@@ -3887,7 +3898,7 @@ snapshots:
fdir: 6.4.6(picomatch@4.0.3) fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
postcss: 8.5.6 postcss: 8.5.6
rollup: 4.45.3 rollup: 4.46.3
tinyglobby: 0.2.14 tinyglobby: 0.2.14
optionalDependencies: optionalDependencies:
'@types/node': 22.16.5 '@types/node': 22.16.5

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