Compare commits

..

33 Commits

Author SHA1 Message Date
Elias Schneider
d8c73ed472 release: 1.8.1 2025-08-24 15:12:14 +02:00
Elias Schneider
5971bfbfa6 fix: migration clears allowed users groups 2025-08-24 15:05:45 +02:00
Alessandro (Ale) Segala
29eacd6424 chore: update issue template (#870) 2025-08-24 14:35:39 +02:00
Elias Schneider
21ca87be38 chore(translations): update translations via Crowdin (#860) 2025-08-24 14:34:44 +02:00
Alessandro (Ale) Segala
1283314f77 fix: wrong column type for reauthentication tokens in Postgres (#869) 2025-08-24 14:34:29 +02:00
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
114 changed files with 3410 additions and 1077 deletions

View File

@@ -36,13 +36,29 @@ body:
value: |
### Additional Information
- type: textarea
id: extra-information
id: version
validations:
required: true
attributes:
label: "Version and Environment"
description: "Please specify the version of Pocket ID, along with any environment-specific configurations, such your reverse proxy, that might be relevant."
label: "Pocket ID Version"
description: "Please specify the version of Pocket ID."
placeholder: "e.g., v0.24.1"
- type: textarea
id: database
validations:
required: true
attributes:
label: "Database"
description: "Please specify the database in use: SQLite or Postgres (including version)."
placeholder: "e.g., SQLite or Postgres 17"
- type: textarea
id: environment
validations:
required: true
attributes:
label: "OS and Environment"
description: "Please include the OS, whether you're using containers (Docker, Podman, etc) along with any environment-specific configurations, such your reverse proxy, that might be relevant."
placeholder: "e.g., Docker on Ubuntu 24.04, served using Traefik"
- type: textarea
id: log-files
validations:

View File

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

View File

@@ -1 +1 @@
1.7.0
1.8.1

View File

@@ -1,3 +1,36 @@
## [](https://github.com/pocket-id/pocket-id/compare/v1.8.0...v) (2025-08-24)
### Bug Fixes
* migration clears allowed users groups ([5971bfb](https://github.com/pocket-id/pocket-id/commit/5971bfbfa66ecfebf2b1c08d34fcbd8c18cdc046))
* wrong column type for reauthentication tokens in Postgres ([#869](https://github.com/pocket-id/pocket-id/issues/869)) ([1283314](https://github.com/pocket-id/pocket-id/commit/1283314f776a0ba43be7d796e7e2243e31f860de))
## [](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)

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:
- [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)
### 2. Setup

View File

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

View File

@@ -1,34 +1,34 @@
module github.com/pocket-id/pocket-id/backend
go 1.24.0
go 1.25
require (
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/imaging v1.6.2
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.21.3
github.com/fxamacker/cbor/v2 v2.7.0
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/go-sqlite v1.21.2
github.com/fxamacker/cbor/v2 v2.9.0
github.com/gin-gonic/gin v1.10.1
github.com/glebarez/go-sqlite v1.22.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-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/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/hashicorp/go-uuid v1.0.3
github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2
github.com/lestrrat-go/jwx/v3 v3.0.1
github.com/lestrrat-go/httprc/v3 v3.0.0
github.com/lestrrat-go/jwx/v3 v3.0.10
github.com/lmittmann/tint v1.1.2
github.com/mattn/go-isatty v0.0.20
github.com/mileusna/useragent v1.3.5
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/spf13/cobra v1.9.1
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/metric v1.35.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/crypto v0.39.0
golang.org/x/image v0.24.0
golang.org/x/text v0.26.0
golang.org/x/time v0.9.0
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
golang.org/x/crypto v0.41.0
golang.org/x/image v0.30.0
golang.org/x/text v0.28.0
golang.org/x/time v0.12.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.1
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.10 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/disintegration/gift v1.1.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // 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/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.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/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // 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/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.5.0 // 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/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/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/mattn/go-sqlite3 v1.14.24 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/prometheus/client_golang v1.22.0 // 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/spf13/pflag v1.0.6 // 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
go.opentelemetry.io/auto/sdk v1.1.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/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.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/rpc v0.0.0-20250218202821-56aae31c358a // 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
modernc.org/libc v1.65.10 // indirect
modernc.org/libc v1.66.7 // indirect
modernc.org/mathutil v1.7.1 // 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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
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.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/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/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.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
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/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
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.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.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-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
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 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
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/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
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/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/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
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 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
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/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8=
github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us=
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
github.com/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
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.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
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.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/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/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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
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.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/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/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/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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/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/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
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-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE=
github.com/lestrrat-go/jwx/v3 v3.0.1 h1:fH3T748FCMbXoF9UXXNS9i0q6PpYyJZK/rKSbkt2guY=
github.com/lestrrat-go/jwx/v3 v3.0.1/go.mod h1:XP2WqxMOSzHSyf3pfibCcfsLqbomxakAnNqiuaH8nwo=
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
github.com/lestrrat-go/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/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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
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.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.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.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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
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/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
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.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/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.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/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.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.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.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.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-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.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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
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-20220722155255-886fb9371eb4/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.7.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.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -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.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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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/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=
@@ -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.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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/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/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/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
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.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
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/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
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/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.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/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
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/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -86,9 +86,6 @@ func connectDatabase() (db *gorm.DB, err error) {
if common.EnvConfig.DbConnectionString == "" {
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()
connString, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
if err != nil {
@@ -123,25 +120,43 @@ func connectDatabase() (db *gorm.DB, err error) {
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) {
if !strings.HasPrefix(connString, "file:") {
connString = "file:" + connString
}
// Check if we're using an in-memory database
isMemoryDB := isSqliteInMemory(connString)
// Parse the connection string
connStringUrl, err := url.Parse(connString)
if err != nil {
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
// This only includes a subset of options, excluding those that are not relevant to us
qs := make(url.Values, len(connStringUrl.Query()))
for k, v := range connStringUrl.Query() {
switch k {
switch strings.ToLower(k) {
case "_auto_vacuum", "_vacuum":
qs.Add("_pragma", "auto_vacuum("+v[0]+")")
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()
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 {

View File

@@ -8,23 +8,93 @@ import (
"github.com/stretchr/testify/require"
)
func TestParseSqliteConnectionString(t *testing.T) {
func TestIsSqliteInMemory(t *testing.T) {
tests := []struct {
name string
input string
expected string
expectedError bool
name string
connStr string
expected 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",
input: "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",
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",
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",
input: "file:invalid#$%^&*@test.db",
expectedError: true,
name: "basic file database",
input: "file:test.db",
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 {
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)
return
}
require.NoError(t, err)
// Parse both URLs to compare components independently
expectedURL, err := url.Parse(tt.expected)
require.NoError(t, err)
resultURL, err := url.Parse(result)
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)
}
compareQueryStrings(t, expectedURL, resultURL)
})
}
}
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
}
listener, err := net.Listen(network, addr)
listener, err := net.Listen(network, addr) //nolint:noctx
if err != nil {
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)
}
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
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)
if err != nil {
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
}

View File

@@ -51,7 +51,7 @@ var oneTimeAccessTokenCmd = &cobra.Command{
}
// 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 {
return fmt.Errorf("failed to generate access token: %w", txErr)
}

View File

@@ -26,7 +26,7 @@ const (
DbProviderSqlite DbProvider = "sqlite"
DbProviderPostgres DbProvider = "postgres"
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 {

View File

@@ -350,6 +350,15 @@ func (e *OidcAuthorizationPendingError) HttpStatusCode() int {
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{}
func (e *OpenSignupDisabledError) Error() string {
@@ -359,3 +368,13 @@ func (e *OpenSignupDisabledError) Error() string {
func (e *OpenSignupDisabledError) HttpStatusCode() int {
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.GET("/oidc/device/info", authMiddleware.WithAdminNotRequired().Add(), oc.getDeviceCodeInfoHandler)
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
group.GET("/oidc/users/:id/clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
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
// @Produce json
// @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"
// @Router /api/oidc/clients/{id} [put]
func (oc *OidcController) updateClientHandler(c *gin.Context) {
var input dto.OidcClientCreateDto
var input dto.OidcClientUpdateDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
@@ -660,7 +662,7 @@ func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
// @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.AuthorizedOidcClientDto]
// @Router /api/oidc/users/me/clients [get]
// @Router /api/oidc/users/me/authorized-clients [get]
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
userID := c.GetString("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[direction] query string false "Sort direction (asc or desc)" default("asc")
// @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) {
userID := c.Param("id")
oc.listAuthorizedClients(c, userID)
@@ -713,7 +715,7 @@ func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
// @Tags OIDC
// @Param clientId path string true "Client ID to revoke authorization for"
// @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) {
clientID := c.Param("clientId")
@@ -728,6 +730,37 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
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) {
userCode := c.Query("code")
if userCode == "" {

View File

@@ -14,6 +14,11 @@ import (
"golang.org/x/time/rate"
)
const (
defaultOneTimeAccessTokenDuration = 15 * time.Minute
defaultSignupTokenDuration = time.Hour
)
// NewUserController creates a new controller for user management endpoints
// @Summary User management controller
// @Description Initializes all user-related API endpoints
@@ -331,10 +336,17 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bo
return
}
var ttl time.Duration
if own {
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 {
_ = c.Error(err)
return
@@ -411,7 +423,11 @@ func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context
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 {
_ = c.Error(err)
return
@@ -526,14 +542,20 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
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 {
_ = c.Error(err)
return
}
var tokenDto dto.SignupTokenDto
if err := dto.MapStruct(signupToken, &tokenDto); err != nil {
err = dto.MapStruct(signupToken, &tokenDto)
if err != nil {
_ = c.Error(err)
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/reauthenticate", authMiddleware.WithAdminNotRequired().Add(), rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), wc.reauthenticateHandler)
group.GET("/webauthn/credentials", authMiddleware.WithAdminNotRequired().Add(), wc.listCredentialsHandler)
group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler)
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, "")
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"`
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
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"`
SmtpHost string `json:"smtpHost"`
SmtpPort string `json:"smtpPort"`

View File

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

View File

@@ -1,14 +1,13 @@
package dto
import (
"time"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
type SignupTokenCreateDto struct {
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
TTL utils.JSONDuration `json:"ttl" binding:"required,ttl"`
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
}
type SignupTokenDto struct {

View File

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

View File

@@ -1,29 +1,52 @@
package dto
import (
"log/slog"
"os"
"regexp"
"time"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/gin-gonic/gin/binding"
"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() {
v, _ := binding.Validator.Engine().(*validator.Validate)
err := v.RegisterValidation("username", validateUsername)
v := binding.Validator.Engine().(*validator.Validate)
// [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 {
slog.Error("Failed to register custom validation", slog.Any("error", err))
os.Exit(1)
return
panic("Failed to register custom validation for username: " + err.Error())
}
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, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, 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),
)
}
@@ -104,6 +105,20 @@ func (j *DbCleanupJobs) clearOidcRefreshTokens(ctx context.Context) error {
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
func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
st := j.db.

View File

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

View File

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

View File

@@ -45,6 +45,15 @@ type PublicKeyCredentialRequestOptions struct {
Timeout time.Duration
}
type ReauthenticationToken struct {
Base
Token string
ExpiresAt datatype.DateTime
UserID string
User User
}
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
// 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
return &model.AppConfig{
// General
AppName: model.AppConfigVariable{Value: "Pocket ID"},
SessionDuration: model.AppConfigVariable{Value: "60"},
EmailsVerified: model.AppConfigVariable{Value: "false"},
DisableAnimations: model.AppConfigVariable{Value: "false"},
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
AccentColor: model.AppConfigVariable{Value: "default"},
AppName: model.AppConfigVariable{Value: "Pocket ID"},
SessionDuration: model.AppConfigVariable{Value: "60"},
EmailsVerified: model.AppConfigVariable{Value: "false"},
DisableAnimations: model.AppConfigVariable{Value: "false"},
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
SignupDefaultUserGroupIDs: model.AppConfigVariable{Value: "[]"},
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
AccentColor: model.AppConfigVariable{Value: "default"},
// Internal
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
LogoLightImageType: model.AppConfigVariable{Value: "svg"},

View File

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

View File

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

View File

@@ -50,6 +50,7 @@ type OidcService struct {
appConfigService *AppConfigService
auditLogService *AuditLogService
customClaimService *CustomClaimService
webAuthnService *WebAuthnService
httpClient *http.Client
jwkCache *jwk.Cache
@@ -62,6 +63,7 @@ func NewOidcService(
appConfigService *AppConfigService,
auditLogService *AuditLogService,
customClaimService *CustomClaimService,
webAuthnService *WebAuthnService,
) (s *OidcService, err error) {
s = &OidcService{
db: db,
@@ -69,6 +71,7 @@ func NewOidcService(
appConfigService: appConfigService,
auditLogService: auditLogService,
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
@@ -123,6 +126,16 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
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 client.IsPublic && input.CodeChallenge == "" {
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
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc"
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && isValidSortDirection {
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
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").
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) {
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.
WithContext(ctx).
Create(&client).
Error
if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return model.OidcClient{}, &common.ClientIdAlreadyExistsError{}
}
return model.OidcClient{}, err
}
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()
defer func() {
tx.Rollback()
@@ -707,7 +725,7 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
return client, nil
}
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientCreateDto) {
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
// Base fields
client.Name = input.Name
client.CallbackURLs = input.CallbackURLs
@@ -715,20 +733,20 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
client.IsPublic = input.IsPublic
// PKCE is required for public clients
client.PkceEnabled = input.IsPublic || input.PkceEnabled
client.RequiresReauthentication = input.RequiresReauthentication
client.LaunchURL = input.LaunchURL
// Credentials
if len(input.Credentials.FederatedIdentities) > 0 {
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
for i, fi := range input.Credentials.FederatedIdentities {
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
Issuer: fi.Issuer,
Audience: fi.Audience,
Subject: fi.Subject,
JWKS: fi.JWKS,
}
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
for i, fi := range input.Credentials.FederatedIdentities {
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
Issuer: fi.Issuer,
Audience: fi.Audience,
Subject: fi.Subject,
JWKS: fi.JWKS,
}
}
}
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
}
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) {
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
if err != nil {
@@ -1462,8 +1555,8 @@ func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *g
// Validate credentials based on the authentication method
switch {
// First, if we have a client secret, we validate it
case input.ClientSecret != "":
// First, if we have a client secret, we validate it unless client is marked as public
case input.ClientSecret != "" && !client.IsPublic:
err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(input.ClientSecret))
if err != nil {
return nil, &common.OidcClientSecretInvalidError{}

View File

@@ -171,8 +171,10 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
// Create the test clients
// 1. Confidential client
confidentialClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
Name: "Confidential Client",
CallbackURLs: []string{"https://example.com/callback"},
OidcClientUpdateDto: dto.OidcClientUpdateDto{
Name: "Confidential Client",
CallbackURLs: []string{"https://example.com/callback"},
},
}, "test-user-id")
require.NoError(t, err)
@@ -182,20 +184,24 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
// 2. Public client
publicClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
Name: "Public Client",
CallbackURLs: []string{"https://example.com/callback"},
IsPublic: true,
OidcClientUpdateDto: dto.OidcClientUpdateDto{
Name: "Public Client",
CallbackURLs: []string{"https://example.com/callback"},
IsPublic: true,
},
}, "test-user-id")
require.NoError(t, err)
// 3. Confidential client with federated identity
federatedClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
Name: "Federated Client",
CallbackURLs: []string{"https://example.com/callback"},
OidcClientUpdateDto: dto.OidcClientUpdateDto{
Name: "Federated Client",
CallbackURLs: []string{"https://example.com/callback"},
},
}, "test-user-id")
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,
CallbackURLs: federatedClient.CallbackURLs,
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
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc"
if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection {
if sortedPaginationRequest.Sort.Column == "userCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
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").
Group("user_groups.id").

View File

@@ -3,6 +3,7 @@ package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -26,20 +27,22 @@ import (
)
type UserService struct {
db *gorm.DB
jwtService *JwtService
auditLogService *AuditLogService
emailService *EmailService
appConfigService *AppConfigService
db *gorm.DB
jwtService *JwtService
auditLogService *AuditLogService
emailService *EmailService
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{
db: db,
jwtService: jwtService,
auditLogService: auditLogService,
emailService: emailService,
appConfigService: appConfigService,
db: db,
jwtService: jwtService,
auditLogService: auditLogService,
emailService: emailService,
appConfigService: appConfigService,
customClaimService: customClaimService,
}
}
@@ -268,9 +271,53 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
} else if err != nil {
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
}
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) {
tx := s.db.Begin()
defer func() {
@@ -348,13 +395,13 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
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()
if isDisabled {
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 {
@@ -374,11 +421,10 @@ func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context
}
}
expiration := time.Now().Add(15 * time.Minute)
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, expiration)
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute)
}
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()
defer func() {
tx.Rollback()
@@ -389,7 +435,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
return err
}
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, expiration, tx)
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
if err != nil {
return err
}
@@ -421,7 +467,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
Code: oneTimeAccessToken,
LoginLink: link,
LoginLinkWithCode: linkWithCode,
ExpirationString: utils.DurationToString(time.Until(expiration).Round(time.Second)),
ExpirationString: utils.DurationToString(ttl),
})
if errInternal != nil {
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
}
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, expiresAt time.Time) (string, error) {
return s.createOneTimeAccessTokenInternal(ctx, userID, expiresAt, s.db)
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error) {
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) {
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, expiresAt)
func (s *UserService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, ttl time.Duration, tx *gorm.DB) (string, error) {
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, ttl)
if err != nil {
return "", err
}
if err := tx.WithContext(ctx).Create(oneTimeAccessToken).Error; err != nil {
err = tx.WithContext(ctx).Create(oneTimeAccessToken).Error
if err != nil {
return "", err
}
@@ -504,7 +551,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
// Fetch the groups based on userGroupIds
var groups []model.UserGroup
if len(userGroupIds) > 0 {
err = tx.
err := tx.
WithContext(ctx).
Where("id IN (?)", userGroupIds).
Find(&groups).
@@ -642,17 +689,14 @@ func (s *UserService) disableUserInternal(ctx context.Context, userID string, tx
Error
}
func (s *UserService) CreateSignupToken(ctx context.Context, expiresAt time.Time, usageLimit int) (model.SignupToken, error) {
return s.createSignupTokenInternal(ctx, expiresAt, usageLimit, s.db)
}
func (s *UserService) createSignupTokenInternal(ctx context.Context, expiresAt time.Time, usageLimit int, tx *gorm.DB) (model.SignupToken, error) {
signupToken, err := NewSignupToken(expiresAt, usageLimit)
func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int) (model.SignupToken, error) {
signupToken, err := NewSignupToken(ttl, usageLimit)
if err != nil {
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
}
@@ -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
}
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
tokenLength := 16
if time.Until(expiresAt) <= 15*time.Minute {
if ttl <= 15*time.Minute {
tokenLength = 6
}
@@ -758,25 +802,27 @@ func NewOneTimeAccessToken(userID string, expiresAt time.Time) (*model.OneTimeAc
return nil, err
}
now := time.Now().Round(time.Second)
o := &model.OneTimeAccessToken{
UserID: userID,
ExpiresAt: datatype.DateTime(expiresAt),
ExpiresAt: datatype.DateTime(now.Add(ttl)),
Token: randomString,
}
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
randomString, err := utils.GenerateRandomAlphanumericString(16)
if err != nil {
return nil, err
}
now := time.Now().Round(time.Second)
token := &model.SignupToken{
Token: randomString,
ExpiresAt: datatype.DateTime(expiresAt),
ExpiresAt: datatype.DateTime(now.Add(ttl)),
UsageLimit: usageLimit,
UsageCount: 0,
}

View File

@@ -221,13 +221,15 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
tx.Rollback()
}()
// Load & delete the session row
var storedSession model.WebauthnSession
err := tx.
WithContext(ctx).
First(&storedSession, "id = ?", sessionID).
Clauses(clause.Returning{}).
Delete(&storedSession, "id = ?", sessionID).
Error
if err != nil {
return model.User{}, "", err
return model.User{}, "", fmt.Errorf("failed to load WebAuthn session: %w", err)
}
session := webauthn.SessionData{
@@ -334,3 +336,136 @@ func (s *WebAuthnService) UpdateCredential(ctx context.Context, userID, credenti
func (s *WebAuthnService) updateWebAuthnConfig() {
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 (
"reflect"
"strconv"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@@ -35,9 +36,7 @@ func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gor
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
if sort.Direction == "" || (sort.Direction != "asc" && sort.Direction != "desc") {
sort.Direction = "asc"
}
sort.Direction = NormalizeSortDirection(sort.Direction)
if sortFieldFound && isSortable {
columnName := CamelCaseToSnakeCase(sort.Column)
@@ -85,3 +84,16 @@ func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (Pagin
ItemsPerPage: pageSize,
}, nil
}
func NormalizeSortDirection(direction string) string {
d := strings.ToLower(strings.TrimSpace(direction))
if d != "asc" && d != "desc" {
return "asc"
}
return d
}
func IsValidSortDirection(direction string) bool {
d := strings.ToLower(strings.TrimSpace(direction))
return d == "asc" || d == "desc"
}

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 uuid 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,177 @@
PRAGMA foreign_keys=OFF;
---------------------------
-- 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;
PRAGMA foreign_keys=ON;
PRAGMA foreign_key_check;

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Kliknutím zkopírujete",
"something_went_wrong": "Něco se pokazilo",
"go_back_to_home": "Přejít zpět domů",
"dont_have_access_to_your_passkey": "Nemáte přístup k Vašemu přístupovému klíči?",
"alternative_sign_in_methods": "Alternativní způsoby přihlášení",
"login_background": "Pozadí přihlašovací stránky",
"logo": "Logo",
"login_code": "Přihlašovací kód",
@@ -276,6 +276,8 @@
"public_clients_description": "Veřejní klienti nemají client secret a místo toho používají PKCE. Povolte to, pokud je váš klient SPA nebo mobilní aplikace.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Exchange je bezpečnostní funkce, která zabraňuje útokům CSRF a narušení autorizačních kódů.",
"requires_reauthentication": "Vyžaduje opětovné ověření",
"requires_users_to_authenticate_again_on_each_authorization": "Vyžaduje, aby se uživatelé při každém autorizačním pokusu znovu ověřili, i když jsou již přihlášeni.",
"name_logo": "Logo {name}",
"change_logo": "Změnit logo",
"upload_logo": "Nahrát logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Kolikrát lze použít registrační token.",
"expires": "Vyprší",
"signup": "Zaregistrovat se",
"user_creation": "Vytvoření uživatele",
"configure_user_creation": "Spravujte nastavení vytváření uživatelů, včetně metod registrace a výchozích oprávnění pro nové uživatele.",
"user_creation_groups_description": "Při registraci automaticky přiřaďte tyto skupiny novým uživatelům.",
"user_creation_claims_description": "Při registraci automaticky přiřaďte tyto vlastní nároky novým uživatelům.",
"user_creation_updated_successfully": "Nastavení pro vytváření uživatelů bylo úspěšně aktualizováno.",
"signup_disabled_description": "Registrace uživatelů jsou kompletně zakázány. Nové uživatelské účty mohou vytvářet pouze správci.",
"signup_requires_valid_token": "Pro vytvoření účtu je vyžadován platný registrační token",
"validating_signup_token": "Ověřování registračního tokenu",
"go_to_login": "Přejít na přihlášení",
@@ -396,7 +404,7 @@
"skip_for_now": "Prozatím přeskočit",
"account_created": "Účet vytvořen",
"enable_user_signups": "Povolit registraci uživatelů",
"enable_user_signups_description": "Určuje, zda by měla být funkce registrace uživatele povolena.",
"enable_user_signups_description": "Rozhodněte, jak se uživatelé mohou registrovat pro nové účty v Pocket ID.",
"user_signups_are_disabled": "Registrace uživatelů jsou v současné době zakázány",
"create_signup_token": "Vytvořit registrační token",
"view_active_signup_tokens": "Zobrazit aktivní registrační tokeny",
@@ -412,7 +420,6 @@
"loading": "Načítání",
"delete_signup_token": "Odstranit registrační token",
"are_you_sure_you_want_to_delete_this_signup_token": "Jste si jisti, že chcete odstranit tento registrační token? Tuto akci nelze vrátit zpět.",
"signup_disabled_description": "Registrace uživatelů jsou kompletně zakázány. Nové uživatelské účty mohou vytvářet pouze správci.",
"signup_with_token": "Zaregistrovat se s tokenem",
"signup_with_token_description": "Uživatelé se mohou zaregistrovat pouze pomocí platného registračního tokenu který byl vytvořen správcem.",
"signup_open": "Otevřená registrace",
@@ -429,5 +436,9 @@
"client_name_description": "Název klienta, který se zobrazuje v uživatelském rozhraní Pocket ID.",
"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_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",
"invalid_client_id": "ID klienta může obsahovat pouze písmena, číslice, podtržítka a pomlčky.",
"custom_client_id_description": "Nastavte vlastní ID klienta, pokud to vyžaduje vaše aplikace. V opačném případě pole nechte prázdné, aby bylo vygenerováno náhodné ID.",
"generated": "Vygenerováno"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Klik for at kopiere",
"something_went_wrong": "Noget gik galt",
"go_back_to_home": "Gå tilbage til hjem",
"dont_have_access_to_your_passkey": "Har du ikke adgang til din adgangsnøgle?",
"alternative_sign_in_methods": "Alternative login-metoder",
"login_background": "Log ind baggrund",
"logo": "Logo",
"login_code": "Loginkode",
@@ -276,6 +276,8 @@
"public_clients_description": "Public-klienter har ikke en klienthemmelighed. De er designet til mobil-, web- og native-apps, hvor hemmeligheder ikke kan opbevares sikkert.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange er en sikkerhedsfunktion, der beskytter mod CSRF- og authorization code-angreb.",
"requires_reauthentication": "Kræver genbekræftelse",
"requires_users_to_authenticate_again_on_each_authorization": "Kræver, at brugerne skal godkende igen ved hver autorisation, selvom de allerede er logget ind.",
"name_logo": "Logo for {name}",
"change_logo": "Skift logo",
"upload_logo": "Upload logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Antal gange, som tilmeldingstokenet kan bruges.",
"expires": "Udløber",
"signup": "Tilmeld",
"user_creation": "Oprettelse af bruger",
"configure_user_creation": "Administrer indstillinger for brugeroprettelse, herunder tilmeldingsmetoder og standardtilladelser for nye brugere.",
"user_creation_groups_description": "Tildel disse grupper automatisk til nye brugere ved tilmelding.",
"user_creation_claims_description": "Tildel disse brugerdefinerede krav automatisk til nye brugere ved tilmelding.",
"user_creation_updated_successfully": "Indstillinger for brugeroprettelse opdateret.",
"signup_disabled_description": "Brugerregistreringer er fuldstændigt deaktiveret. Kun administratorer kan oprette nye brugerkonti.",
"signup_requires_valid_token": "Der kræves en gyldig tilmeldingstoken for at oprette en konto.",
"validating_signup_token": "Validering af tilmeldingstoken",
"go_to_login": "Gå til login",
@@ -396,7 +404,7 @@
"skip_for_now": "Spring over for nu",
"account_created": "Konto oprettet",
"enable_user_signups": "Aktiver brugerregistrering",
"enable_user_signups_description": "Om brugerregistreringsfunktionen skal være aktiveret.",
"enable_user_signups_description": "Bestem, hvordan brugere kan oprette nye konti i Pocket ID.",
"user_signups_are_disabled": "Brugerregistrering er i øjeblikket deaktiveret.",
"create_signup_token": "Opret tilmeldingstoken",
"view_active_signup_tokens": "Vis aktive tilmeldingstokener",
@@ -412,7 +420,6 @@
"loading": "Indlæsning",
"delete_signup_token": "Slet tilmeldingstoken",
"are_you_sure_you_want_to_delete_this_signup_token": "Er du sikker på, at du vil slette denne tilmeldingstoken? Denne handling kan ikke fortrydes.",
"signup_disabled_description": "Brugerregistreringer er fuldstændigt deaktiveret. Kun administratorer kan oprette nye brugerkonti.",
"signup_with_token": "Tilmeld dig med token",
"signup_with_token_description": "Brugere kan kun tilmelde sig ved hjælp af en gyldig tilmeldingstoken, der er oprettet af en administrator.",
"signup_open": "Åben tilmelding",
@@ -429,5 +436,9 @@
"client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.",
"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_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",
"invalid_client_id": "Kunde-ID må kun indeholde bogstaver, tal, understregninger og bindestreger.",
"custom_client_id_description": "Indstil et brugerdefineret klient-id, hvis dette kræves af din applikation. Ellers skal du lade feltet være tomt for at generere et tilfældigt id.",
"generated": "Genereret"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Zum Kopieren klicken",
"something_went_wrong": "Etwas ist schiefgelaufen",
"go_back_to_home": "Zurück zur Startseite",
"dont_have_access_to_your_passkey": "Du hast keinen Zugriff auf deinen Passkey?",
"alternative_sign_in_methods": "Andere Anmeldemöglichkeiten",
"login_background": "Login Hintergrund",
"logo": "Logo",
"login_code": "Anmeldecode",
@@ -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.",
"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.",
"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",
"change_logo": "Logo ändern",
"upload_logo": "Logo hochladen",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Wie oft der Anmeldetoken benutzt werden kann.",
"expires": "Läuft ab",
"signup": "Anmelden",
"user_creation": "Benutzererstellung",
"configure_user_creation": "Verwalte die Einstellungen für die Benutzererstellung, einschließlich der Anmeldemethoden und Standardberechtigungen für neue Benutzer.",
"user_creation_groups_description": "Weise diese Gruppen neuen Benutzern bei der Anmeldung automatisch zu.",
"user_creation_claims_description": "Weise diese benutzerdefinierten Ansprüche neuen Benutzern bei der Anmeldung automatisch zu.",
"user_creation_updated_successfully": "Einstellungen für die Benutzererstellung erfolgreich aktualisiert.",
"signup_disabled_description": "Benutzeranmeldungen sind komplett deaktiviert. Nur Admins können neue Benutzerkonten erstellen.",
"signup_requires_valid_token": "Zum Erstellen eines Kontos brauchst du einen gültigen Anmeldetoken.",
"validating_signup_token": "Anmeldungstoken bestätigen",
"go_to_login": "Zum Login gehen",
@@ -396,7 +404,7 @@
"skip_for_now": "Jetzt überspringen",
"account_created": "Konto erstellt",
"enable_user_signups": "Benutzeranmeldungen aktivieren",
"enable_user_signups_description": "Ob die Funktion zur Benutzeranmeldung aktiviert werden soll.",
"enable_user_signups_description": "Entscheide, wie sich Leute für neue Konten in Pocket ID anmelden können.",
"user_signups_are_disabled": "Benutzeranmeldungen sind im Moment deaktiviert.",
"create_signup_token": "Anmeldungstoken erstellen",
"view_active_signup_tokens": "Aktive Anmeldetoken anzeigen",
@@ -412,7 +420,6 @@
"loading": "Laden",
"delete_signup_token": "Anmeldungstoken löschen",
"are_you_sure_you_want_to_delete_this_signup_token": "Willst du diesen Anmeldetoken wirklich löschen? Das kannst du nicht rückgängig machen.",
"signup_disabled_description": "Benutzeranmeldungen sind komplett deaktiviert. Nur Admins können neue Benutzerkonten erstellen.",
"signup_with_token": "Mit Token anmelden",
"signup_with_token_description": "Benutzer können sich nur mit einem gültigen Anmeldetoken anmelden, das von einem Administrator erstellt wurde.",
"signup_open": "Anmeldung offen",
@@ -429,5 +436,9 @@
"client_name_description": "Der Name des Clients, der in der Pocket ID-Benutzeroberfläche angezeigt wird.",
"revoke_access": "Zugriff widerrufen",
"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",
"invalid_client_id": "Die Kunden-ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche haben.",
"custom_client_id_description": "Gib eine eigene Client-ID ein, wenn deine App das braucht. Ansonsten lass das Feld leer, damit eine zufällige ID generiert wird.",
"generated": "Generiert"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Click to copy",
"something_went_wrong": "Something went wrong",
"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",
"logo": "Logo",
"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.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
"requires_reauthentication": "Requires 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",
"change_logo": "Change 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.",
"expires": "Expires",
"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",
"validating_signup_token": "Validating signup token",
"go_to_login": "Go to login",
@@ -396,7 +404,7 @@
"skip_for_now": "Skip for now",
"account_created": "Account Created",
"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",
"create_signup_token": "Create Signup Token",
"view_active_signup_tokens": "View Active Signup Tokens",
@@ -412,7 +420,6 @@
"loading": "Loading",
"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.",
"signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.",
"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_open": "Open Signup",
@@ -429,5 +436,9 @@
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
"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_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

@@ -23,7 +23,7 @@
"click_to_copy": "Haz clic para copiar",
"something_went_wrong": "Algo ha salido mal",
"go_back_to_home": "Volver al Inicio",
"dont_have_access_to_your_passkey": "¿No tiene acceso a su Passkey?",
"alternative_sign_in_methods": "Métodos alternativos de inicio de sesión",
"login_background": "Fondo de página de acceso",
"logo": "Logotipo",
"login_code": "Código de inicio de sesión",
@@ -276,6 +276,8 @@
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "El intercambio de claves públicas es una función de seguridad que evita los ataques CSRF y la interceptación de códigos de autorización.",
"requires_reauthentication": "Requiere reautenticación",
"requires_users_to_authenticate_again_on_each_authorization": "Requiere que los usuarios se autentiquen de nuevo en cada autorización, incluso si ya han iniciado sesión.",
"name_logo": "{name} logotipo",
"change_logo": "Cambiar logotipo",
"upload_logo": "Subir Logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Número de veces que se puede utilizar el token de registro.",
"expires": "Caduca",
"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.",
"validating_signup_token": "Validación del token de registro",
"go_to_login": "Ir al inicio de sesión",
@@ -412,7 +420,6 @@
"loading": "Cargando",
"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.",
"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_description": "Los usuarios solo pueden registrarse utilizando un token de registro válido creado por un administrador.",
"signup_open": "Inscripción abierta",
@@ -429,5 +436,9 @@
"client_name_description": "El nombre del cliente que aparece en la interfaz de usuario de Pocket ID.",
"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_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",
"invalid_client_id": "El ID de cliente solo puede contener letras, números, guiones bajos y guiones.",
"custom_client_id_description": "Establece un ID de cliente personalizado si tu aplicación lo requiere. De lo contrario, déjalo en blanco para generar uno aleatorio.",
"generated": "Generado"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Cliquer pour copier",
"something_went_wrong": "Quelque chose n'a pas fonctionné",
"go_back_to_home": "Retourner à l'accueil",
"dont_have_access_to_your_passkey": "Vous n'avez pas accès à votre clé d'accès ?",
"alternative_sign_in_methods": "Autres façons de se connecter",
"login_background": "Arrière-plan de connexion",
"logo": "Logo",
"login_code": "Code de connexion",
@@ -276,6 +276,8 @@
"public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et linterception de code dautorisation.",
"requires_reauthentication": "Nécessite une nouvelle authentification",
"requires_users_to_authenticate_again_on_each_authorization": "Demande aux utilisateurs de se connecter à nouveau à chaque autorisation, même s'ils sont déjà connectés.",
"name_logo": "Logo {name}",
"change_logo": "Changer le logo",
"upload_logo": "Télécharger un logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Nombre de fois que le jeton d'inscription peut être utilisé.",
"expires": "Expire",
"signup": "S'inscrire",
"user_creation": "Création d'un utilisateur",
"configure_user_creation": "Gère les paramètres de création des utilisateurs, comme les méthodes d'inscription et les autorisations par défaut pour les nouveaux utilisateurs.",
"user_creation_groups_description": "Attribuez automatiquement ces groupes aux nouveaux utilisateurs lors de leur inscription.",
"user_creation_claims_description": "Attribuez automatiquement ces revendications personnalisées aux nouveaux utilisateurs lors de leur inscription.",
"user_creation_updated_successfully": "Les paramètres de création d'utilisateur ont été mis à jour.",
"signup_disabled_description": "Les inscriptions utilisateur sont complètement désactivées. Seuls les administrateurs peuvent créer de nouveaux comptes utilisateur.",
"signup_requires_valid_token": "Un jeton d'inscription valide est requis pour créer un compte.",
"validating_signup_token": "Validation du jeton d'inscription",
"go_to_login": "Aller à la connexion",
@@ -396,7 +404,7 @@
"skip_for_now": "Ignorer pour le moment",
"account_created": "Compte créé",
"enable_user_signups": "Activer les inscriptions utilisateur",
"enable_user_signups_description": "Détermine si la fonctionnalité d'inscription des utilisateurs doit être activée.",
"enable_user_signups_description": "Décide comment les utilisateurs peuvent créer de nouveaux comptes dans Pocket ID.",
"user_signups_are_disabled": "Les inscriptions utilisateur sont actuellement désactivées",
"create_signup_token": "Créer un jeton d'inscription",
"view_active_signup_tokens": "Voir les jetons d'inscription actifs",
@@ -412,7 +420,6 @@
"loading": "Chargement",
"delete_signup_token": "Supprimer le jeton d'inscription",
"are_you_sure_you_want_to_delete_this_signup_token": "Êtes-vous sûr de vouloir supprimer ce jeton d'inscription ? Cette action est irréversible.",
"signup_disabled_description": "Les inscriptions utilisateur sont complètement désactivées. Seuls les administrateurs peuvent créer de nouveaux comptes utilisateur.",
"signup_with_token": "Inscription avec jeton",
"signup_with_token_description": "Les utilisateurs ne peuvent s'inscrire qu'en utilisant un jeton d'inscription valide créé par un administrateur.",
"signup_open": "Inscription ouverte",
@@ -429,5 +436,9 @@
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
"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_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",
"invalid_client_id": "L'ID client ne peut contenir que des lettres, des chiffres, des traits de soulignement et des tirets.",
"custom_client_id_description": "Définissez un identifiant client personnalisé si votre application l'exige. Sinon, laissez ce champ vide pour qu'un identifiant aléatoire soit généré.",
"generated": "Généré"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Clicca per copiare",
"something_went_wrong": "Qualcosa è andato storto",
"go_back_to_home": "Torna alla home",
"dont_have_access_to_your_passkey": "Non hai accesso alla tua passkey?",
"alternative_sign_in_methods": "Metodi di accesso alternativi",
"login_background": "Sfondo di accesso",
"logo": "Logo",
"login_code": "Codice di accesso",
@@ -276,6 +276,8 @@
"public_clients_description": "I client pubblici non hanno un client secret e utilizzano PKCE. Abilita questa opzione se il tuo client è una SPA o un'app mobile.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Il Public Key Code Exchange è una funzionalità di sicurezza per prevenire attacchi CSRF e intercettazione del codice di autorizzazione.",
"requires_reauthentication": "È necessario effettuare nuovamente l'autenticazione",
"requires_users_to_authenticate_again_on_each_authorization": "Chiede agli utenti di fare di nuovo l'autenticazione ogni volta che autorizzano qualcosa, anche se hanno già fatto l'accesso.",
"name_logo": "Logo di {name}",
"change_logo": "Cambia Logo",
"upload_logo": "Carica Logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Numero di volte che il codice d'iscrizione può essere usato.",
"expires": "Scadenza",
"signup": "Registrati",
"user_creation": "Creazione utente",
"configure_user_creation": "Gestisci le impostazioni per creare nuovi utenti, come i metodi di registrazione e i permessi di default per quelli nuovi.",
"user_creation_groups_description": "Assegna automaticamente questi gruppi ai nuovi utenti al momento della registrazione.",
"user_creation_claims_description": "Assegna automaticamente queste richieste personalizzate ai nuovi utenti al momento della registrazione.",
"user_creation_updated_successfully": "Le impostazioni per creare un utente sono state aggiornate.",
"signup_disabled_description": "Le iscrizioni utente sono completamente disabilitate. Solo gli amministratori possono creare nuovi account utente.",
"signup_requires_valid_token": "È necessario un codice d'iscrizione valido per creare un account",
"validating_signup_token": "Convalida codice d'iscrizione",
"go_to_login": "Vai alla login",
@@ -396,7 +404,7 @@
"skip_for_now": "Salta per ora",
"account_created": "Account creato",
"enable_user_signups": "Abilita Iscrizioni Utente",
"enable_user_signups_description": "Indica se la funzionalità di registrazione utente deve essere abilitata.",
"enable_user_signups_description": "Decidi come gli utenti possono registrarsi per creare nuovi account in Pocket ID.",
"user_signups_are_disabled": "Le iscrizioni utente sono attualmente disattivate",
"create_signup_token": "Crea Codice d'iscrizione",
"view_active_signup_tokens": "Visualizza codici d'iscrizione attivi",
@@ -412,7 +420,6 @@
"loading": "Caricamento",
"delete_signup_token": "Elimina Codice d'iscrizione",
"are_you_sure_you_want_to_delete_this_signup_token": "Sei sicuro di voler eliminare questo codice d'iscrizione? Questa azione non può essere annullata.",
"signup_disabled_description": "Le iscrizioni utente sono completamente disabilitate. Solo gli amministratori possono creare nuovi account utente.",
"signup_with_token": "Registrati con codice",
"signup_with_token_description": "Gli utenti possono registrarsi solo usando un codice d'iscrizione valido, creato da un amministratore.",
"signup_open": "Apri Registrazione",
@@ -429,5 +436,9 @@
"client_name_description": "Il nome del cliente che appare nell'interfaccia utente Pocket ID.",
"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_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",
"invalid_client_id": "L'ID cliente può contenere solo lettere, numeri, trattini bassi e trattini.",
"custom_client_id_description": "Imposta un ID cliente personalizzato se la tua app lo richiede. Altrimenti, lascia vuoto per generarne uno casuale.",
"generated": "Generato"
}

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

@@ -0,0 +1,444 @@
{
"$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": "홈으로 돌아가기",
"alternative_sign_in_methods": "대체 로그인 방법",
"login_background": "로그인 배경",
"logo": "로고",
"login_code": "로그인 코드",
"create_a_login_code_to_sign_in_without_a_passkey_once": "패스키 없이 한 번 로그인 할 수 있는 로그인 코드를 생성합니다.",
"one_hour": "1시간",
"twelve_hours": "12시간",
"one_day": "1일",
"one_week": "1주",
"one_month": "1달",
"expiration": "만료",
"generate_code": "코드 생성",
"name": "이름",
"browser_unsupported": "지원되지 않는 브라우저",
"this_browser_does_not_support_passkeys": "이 브라우저는 패스키를 지원하지 않습니다. 다른 로그인 방법을 사용하세요.",
"an_unknown_error_occurred": "알 수 없는 오류 발생",
"authentication_process_was_aborted": "인증 프로세스가 중단되었습니다",
"error_occurred_with_authenticator": "인증기에서 오류가 발생했습니다",
"authenticator_does_not_support_discoverable_credentials": "인증기가 발견 가능한 자격 증명을 지원하지 않습니다",
"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 및 승인 코드 가로채기 공격을 방지하기 위한 보안 기능입니다.",
"requires_reauthentication": "재인증이 필요합니다.",
"requires_users_to_authenticate_again_on_each_authorization": "사용자가 이미 로그인한 상태에서도 각 권한 부여 시마다 다시 인증을 요구합니다.",
"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": "계정 생성",
"user_creation": "사용자 생성",
"configure_user_creation": "사용자 생성 설정을 관리합니다. 이에는 신규 사용자 등록 방법 및 신규 사용자의 기본 권한이 포함됩니다.",
"user_creation_groups_description": "새 사용자가 가입할 때 이 그룹을 자동으로 할당합니다.",
"user_creation_claims_description": "새 사용자가 가입할 때 이 사용자 정의 클레임을 자동으로 할당합니다.",
"user_creation_updated_successfully": "사용자 생성 설정 업데이트가 성공적으로 완료되었습니다.",
"signup_disabled_description": "계정 생성이 완전히 비활성화됩니다. 새로운 사용자 계정은 관리자만 생성할 수 있습니다.",
"signup_requires_valid_token": "계정을 생성하려면 유효한 계정 생성 토큰이 필요합니다",
"validating_signup_token": "계정 생성 토큰 검증",
"go_to_login": "로그인으로 이동",
"signup_to_appname": "{appName} 계정 생성하기",
"create_your_account_to_get_started": "계정을 만들어 시작하세요.",
"initial_account_creation_description": "시작하기 위해 계정을 만드세요. 패스키를 나중에 설정할 수 있습니다.",
"setup_your_passkey": "패스키 설정",
"create_a_passkey_to_securely_access_your_account": "계정에 안전하게 접근하기 위해 패스키를 생성하세요. 이 패스키는 로그인을 위한 주요 방법으로 사용됩니다.",
"skip_for_now": "지금은 건너뛰기",
"account_created": "계정 생성됨",
"enable_user_signups": "계정 생성 활성화",
"enable_user_signups_description": "Pocket ID에서 사용자가 새로운 계정을 생성하는 방법을 결정하세요.",
"user_signups_are_disabled": "계정 생성이 현재 비활성화되었습니다",
"create_signup_token": "계정 생성 토큰 생성",
"view_active_signup_tokens": "활성 계정 생성 토큰 보기",
"manage_signup_tokens": "계정 생성 토큰 관리",
"view_and_manage_active_signup_tokens": "활성 계정 생성 토큰을 조회하고 관리합니다.",
"signup_token_deleted_successfully": "계정 생성 토큰이 성공적으로 삭제되었습니다.",
"expired": "만료됨",
"used_up": "사용 완료",
"active": "활성",
"usage": "사용량",
"created": "생성일",
"token": "토큰",
"loading": "불러오는 중",
"delete_signup_token": "계정 생성 토큰 삭제",
"are_you_sure_you_want_to_delete_this_signup_token": "이 계정 생성 토큰을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"signup_with_token": "토큰으로 계정 생성",
"signup_with_token_description": "사용자는 관리자가 생성한 유효한 계정 생성 토큰을 사용해야 가입할 수 있습니다.",
"signup_open": "계정 생성 허용",
"signup_open_description": "누구나 제한 없이 새로운 계정을 생성할 수 있습니다.",
"of": "의",
"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} 전에 로그인함",
"invalid_client_id": "고객 ID에는 영문자, 숫자, 밑줄, 하이픈만 포함될 수 있습니다.",
"custom_client_id_description": "응용 프로그램에서 이 정보가 필요한 경우 사용자 정의 클라이언트 ID를 설정하세요. 그렇지 않은 경우 빈 상태로 두면 무작위로 생성됩니다.",
"generated": "생성됨"
}

View File

@@ -3,17 +3,17 @@
"my_account": "Mijn account",
"logout": "Uitloggen",
"confirm": "Bevestigen",
"docs": "Documenten",
"docs": "Documentatie",
"key": "Sleutel",
"value": "Waarde",
"remove_custom_claim": "Aangepaste claim verwijderen",
"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_file": "Selecteer bestand",
"profile_picture": "Profielfoto",
"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.",
"items_per_page": "Aantal per pagina",
"no_items_found": "Geen items gevonden",
@@ -22,8 +22,8 @@
"copied": "Gekopieerd",
"click_to_copy": "Klik om te kopiëren",
"something_went_wrong": "Er is iets misgegaan",
"go_back_to_home": "Ga terug naar huis",
"dont_have_access_to_your_passkey": "Heeft u geen toegang tot uw toegangscode?",
"go_back_to_home": "Terug naar beginpagina",
"alternative_sign_in_methods": "Andere manieren om in te loggen",
"login_background": "Inlogachtergrond",
"logo": "Logo",
"login_code": "Inlogcode",
@@ -42,49 +42,49 @@
"authentication_process_was_aborted": "Het authenticatieproces is afgebroken",
"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_resident_keys": "De authenticator ondersteunt geen residente sleutels",
"passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd",
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen vaste sleutels",
"passkey_was_previously_registered": "Deze passkey is eerder geregistreerd",
"authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen",
"authenticator_timed_out": "De authenticator is verlopen",
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met uw beheerder.",
"sign_in_to": "Meld u aan bij {name}",
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met de beheerder.",
"sign_in_to": "Meld je aan bij {name}",
"client_not_found": "Client niet gevonden",
"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",
"view_your_email_address": "Bekijk uw e-mailadres",
"view_your_email_address": "Bekijk je e-mailadres",
"profile": "Profiel",
"view_your_profile_information": "Bekijk uw profielgegevens",
"view_your_profile_information": "Bekijk je profielgegevens",
"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",
"sign_in": "Aanmelden",
"try_again": "Probeer het opnieuw",
"client_logo": "Client logo",
"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> ?",
"sign_in_to_appname": "Meld u aan bij {appName}",
"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 je aan bij {appName}",
"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",
"please_try_again": "Probeer het opnieuw.",
"continue": "Doorgaan",
"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.",
"use_your_passkey_instead": "Wilt u in plaats daarvan uw toegangscode gebruiken?",
"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 passkey, kun je je op een van de volgende manieren aanmelden.",
"use_your_passkey_instead": "Wil je in plaats daarvan je passkey gebruiken?",
"email_login": "E-mail inloggen",
"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.",
"go_back": "Ga 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.",
"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 deze in het systeem voorkomt.",
"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.",
"your_email": "Uw e-mail",
"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": "Je e-mail",
"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",
"invalid_redirect_url": "Ongeldige omleidings-URL",
"audit_log": "Audit logboek",
"audit_log": "Activiteitenlogboek",
"users": "Gebruikers",
"user_groups": "Gebruikersgroepen",
"oidc_clients": "OIDC-clients",
@@ -93,52 +93,52 @@
"settings": "Instellingen",
"update_pocket_id": "Pocket-ID bijwerken",
"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",
"event": "Evenement",
"event": "Activiteit",
"approximate_location": "Geschatte locatie",
"ip_address": "IP-adres",
"device": "Apparaat",
"client": "Cliënt",
"client": "Client",
"unknown": "Onbekend",
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
"account_settings": "Accountinstellingen",
"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.",
"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.",
"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 passkey geconfigureerd",
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één passkey toe te voegen om te voorkomen dat je de toegang tot uw account verliest.",
"account_details": "Accountgegevens",
"passkeys": "Toegangscodes",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.",
"passkeys": "Passkeys",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de passkeys waarmee je jezelf kunt verifiëren.",
"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": "Creëren",
"create": "Aanmaken",
"first_name": "Voornaam",
"last_name": "Achternaam",
"username": "Gebruikersnaam",
"save": "Opslaan",
"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",
"added_on": "Toegevoegd op",
"rename": "Hernoemen",
"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",
"delete_passkey_name": "Verwijder {passkeyName}",
"passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt",
"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_passkey": "Naam passkey",
"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",
"add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.",
"add_api_key": "API-sleutel toevoegen",
"manage_api_keys": "API-sleutels beheren",
"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",
"api_key": "API-sleutel",
"close": "Dichtbij",
"close": "Sluiten",
"name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.",
"expires_at": "Verloopt op",
"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",
"revoke_api_key": "API-sleutel intrekken",
"never": "Nooit",
"revoke": "Herroepen",
"revoke": "Intrekken",
"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",
"actions": "Acties",
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
"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",
"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",
"update": "Update",
"email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt",
"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",
"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.",
"smtp_configuration": "SMTP-configuratie",
"smtp_host": "SMTP-host",
"smtp_port": "SMTP-poort",
"smtp_user": "SMTP-gebruiker",
"smtp_password": "SMTP-wachtwoord",
"smtp_from": "SMTP van",
"smtp_from": "SMTP-afzender",
"smtp_tls_option": "SMTP TLS-optie",
"email_tls_option": "E-mail TLS-optie",
"skip_certificate_verification": "Certificaatverificatie overslaan",
@@ -178,15 +178,15 @@
"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.",
"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",
"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",
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
"application_name": "Toepassingsnaam",
"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.",
"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.",
"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.",
@@ -198,26 +198,26 @@
"ldap_bind_dn": "LDAP Bind-DN",
"ldap_bind_password": "LDAP Bind-wachtwoord",
"ldap_base_dn": "LDAP-basis-DN",
"user_search_filter": "Gebruikerszoekfilter",
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee u gebruikers kunt zoeken/synchroniseren.",
"groups_search_filter": "Groepen Zoekfilter",
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee u groepen kunt zoeken/synchroniseren.",
"user_search_filter": "Zoekfilter gebruikers",
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee je gebruikers kunt zoeken/synchroniseren.",
"groups_search_filter": "Zoekfilter groepen",
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee je groepen kunt zoeken/synchroniseren.",
"attribute_mapping": "Attribuuttoewijzing",
"user_unique_identifier_attribute": "Gebruiker uniek identificatiekenmerk",
"the_value_of_this_attribute_should_never_change": "De waarde van dit kenmerk mag nooit veranderen.",
"username_attribute": "Gebruikersnaam Attribuut",
"user_mail_attribute": "Gebruikersmailkenmerk",
"user_first_name_attribute": "Gebruikersvoornaam Attribuut",
"user_last_name_attribute": "Gebruikersnaam Achternaam Attribuut",
"user_profile_picture_attribute": "Gebruikersprofielfoto-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.",
"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.",
"group_unique_identifier_attribute": "Groeps uniek identificatiekenmerk",
"group_name_attribute": "Groepsnaam Attribuut",
"user_unique_identifier_attribute": "Uniek gebruikersidentificatie attribuut",
"the_value_of_this_attribute_should_never_change": "De waarde van dit attribuut mag nooit veranderen.",
"username_attribute": "Gebruikersnaam attribuut",
"user_mail_attribute": "Gebruikers e-mail attribuut",
"user_first_name_attribute": "Gebruikers voornaam attribuut",
"user_last_name_attribute": "Gebruikers achternaam 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 attribuut kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.",
"group_members_attribute": "Groepsleden attribuut",
"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": "Uniek groepsidentificatie attribuut",
"group_name_attribute": "Groepsnaam attribuut",
"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.",
"disable": "Uitzetten",
"disable": "Uitschakelen",
"sync_now": "Nu synchroniseren",
"enable": "Inschakelen",
"user_created_successfully": "Gebruiker succesvol aangemaakt",
@@ -228,7 +228,7 @@
"admin_privileges": "Beheerdersrechten",
"admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.",
"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",
"role": "Rol",
"source": "Bron",
@@ -236,7 +236,7 @@
"user": "Gebruiker",
"local": "Lokaal",
"toggle_menu": "Menu wisselen",
"edit": "Bewerking",
"edit": "Bewerk",
"user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt",
"user_updated_successfully": "Gebruiker succesvol bijgewerkt",
"custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt",
@@ -252,9 +252,9 @@
"manage_user_groups": "Gebruikersgroepen beheren",
"friendly_name": "Vriendelijke naam",
"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}",
"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_count": "Gebruikersaantal",
"user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt",
@@ -268,19 +268,21 @@
"add_oidc_client": "OIDC-client toevoegen",
"manage_oidc_clients": "OIDC-clients beheren",
"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",
"callback_urls": "Callback-URL's",
"logout_callback_urls": "Callback-URL's voor afmelden",
"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",
"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.",
"requires_reauthentication": "Je moet opnieuw inloggen",
"requires_users_to_authenticate_again_on_each_authorization": "Gebruikers moeten bij elke autorisatie opnieuw inloggen, zelfs als ze al ingelogd zijn.",
"name_logo": "{name} logo",
"change_logo": "Logo wijzigen",
"upload_logo": "Logo uploaden",
"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",
"authorization_url": "Autorisatie-URL",
"oidc_discovery_url": "OIDC-ontdekkings-URL",
@@ -292,12 +294,12 @@
"disabled": "Uitgeschakeld",
"oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt",
"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",
"new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt",
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
"oidc_client_name": "OIDC-client {name}",
"client_id": "Client id",
"client_id": "Client ID",
"client_secret": "Client geheim",
"show_more_details": "Meer details weergeven",
"allowed_user_groups": "Toegestane gebruikersgroepen",
@@ -308,50 +310,50 @@
"background_image": "Achtergrondfoto",
"language": "Taal",
"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_to_default": "Standaardinstellingen herstellen",
"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.",
"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>.",
"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 u een fout vindt, kun je altijd helpen met de vertaling op <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
"personal": "Persoonlijk",
"global": "Globaal",
"all_users": "Alle gebruikers",
"all_events": "Alle activiteiten",
"all_clients": "Alle clients",
"all_locations": "Alle locaties",
"global_audit_log": "Algemeen audit logboek",
"global_audit_log": "Globaal activiteitenlogboek",
"see_all_account_activities_from_the_last_3_months": "Bekijk alle gebruikersactiviteit van de afgelopen 3 maanden.",
"token_sign_in": "Inloggen met token",
"client_authorization": "Client autorisatie",
"new_client_authorization": "Nieuwe clientautorisatie",
"disable_animations": "Animatie uitzetten",
"disable_animations": "Animaties uitzetten",
"turn_off_ui_animations": "Zet alle animaties in de gebruikersinterface uit.",
"user_disabled": "Account uitgeschakeld",
"disabled_users_cannot_log_in_or_use_services": "Gebruikers met een handicap kunnen niet inloggen of diensten gebruiken.",
"user_disabled_successfully": "Je bent nu uitgelogd.",
"user_enabled_successfully": "Je bent nu aangemeld.",
"disabled_users_cannot_log_in_or_use_services": "Uitgeschakelde gebruikers kunnen niet inloggen of diensten gebruiken.",
"user_disabled_successfully": "Gebruiker is succesvol uitgeschakeld.",
"user_enabled_successfully": "Gebruiker is succesvol geactiveerd.",
"status": "Status",
"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.",
"ldap_soft_delete_users": "Voorkom dat gebruikers met een handicap toegang krijgen tot LDAP.",
"ldap_soft_delete_users_description": "Als dit is ingeschakeld, worden gebruikers die uit LDAP worden verwijderd, uitgeschakeld in plaats van uit het systeem verwijderd.",
"login_code_email_success": "De inlogcode is naar je gestuurd.",
"send_email": "E-mail sturen",
"show_code": "Code tonen",
"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.",
"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.",
"disable_firstname_lastname": "{firstName} {lastName} uitschakelen",
"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 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 daadwerkelijk uit het systeem verwijderd.",
"login_code_email_success": "De inlogcode is naar de gebruiker gestuurd.",
"send_email": "Verstuur e-mail",
"show_code": "Toon code",
"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 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",
"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",
"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",
"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_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",
"show_advanced_options": "Geavanceerde opties weergeven",
"hide_advanced_options": "Verberg geavanceerde opties",
@@ -378,56 +380,65 @@
"custom_accent_color": "Aangepaste accentkleur",
"custom_accent_color_description": "Voer een eigen kleur in met een geldige CSS-kleurcode (bijvoorbeeld hex, rgb, hsl).",
"color_value": "Kleurwaarde",
"apply": "Solliciteren",
"apply": "Toepassen",
"signup_token": "Aanmeldingstoken",
"create_a_signup_token_to_allow_new_user_registration": "Maak een aanmeldingstoken aan om nieuwe gebruikers te laten registreren.",
"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",
"signup": "Aanmelden",
"signup_requires_valid_token": "Je hebt een geldige registratietoken nodig om een account aan te maken.",
"user_creation": "Gebruikers aanmaken",
"configure_user_creation": "Beheer de instellingen voor het aanmaken van gebruikers, zoals hoe mensen zich kunnen aanmelden en wat nieuwe gebruikers standaard kunnen doen.",
"user_creation_groups_description": "Wijs deze groepen automatisch toe aan nieuwe gebruikers bij aanmelding.",
"user_creation_claims_description": "Wijs deze aangepaste claims automatisch toe aan nieuwe gebruikers bij aanmelding.",
"user_creation_updated_successfully": "Instellingen voor het aanmaken van gebruikers zijn bijgewerkt.",
"signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
"signup_requires_valid_token": "U heeft een geldige registratietoken nodig om een account aan te maken.",
"validating_signup_token": "Inlogtoken checken",
"go_to_login": "Ga naar inloggen",
"signup_to_appname": "Meld je aan voor {appName}",
"create_your_account_to_get_started": "Maak je account aan om te beginnen.",
"initial_account_creation_description": "Maak een account aan om te beginnen. Je kunt later een wachtwoord instellen.",
"setup_your_passkey": "Stel je 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.",
"signup_to_appname": "Meld u aan voor {appName}",
"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. U kunt later een wachtwoord instellen.",
"setup_your_passkey": "Stel uw passkey in",
"create_a_passkey_to_securely_access_your_account": "Maak een passkey 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",
"account_created": "Account aangemaakt",
"enable_user_signups": "Gebruikersregistratie inschakelen",
"enable_user_signups_description": "Of de functie voor gebruikersregistratie moet worden ingeschakeld.",
"user_signups_are_disabled": "Je kunt nu niet aanmelden.",
"enable_user_signups_description": "Bepaal hoe mensen zich kunnen aanmelden voor nieuwe accounts in Pocket ID.",
"user_signups_are_disabled": "Gebruikersregistraties zijn nu uitgeschakeld.",
"create_signup_token": "Aanmeldingstoken maken",
"view_active_signup_tokens": "Actieve aanmeldingstokens bekijken",
"manage_signup_tokens": "Aanmeldingstokens beheren",
"view_and_manage_active_signup_tokens": "Bekijk en beheer actieve aanmeldingstokens.",
"signup_token_deleted_successfully": "Aanmeldingstoken succesvol verwijderd.",
"expired": "Verlopen",
"used_up": "Opgebruikt",
"used_up": "Verbruikt",
"active": "Actief",
"usage": "Gebruik",
"created": "Gemaakt",
"token": "Token",
"loading": "Bezig met laden",
"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.",
"signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
"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_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_description": "Iedereen kan zonder beperkingen een nieuw account aanmaken.",
"of": "van",
"skip_passkey_setup": "Pas 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": "Sla de instellingen voor de toegangssleutel over",
"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",
"no_apps_available": "Geen apps beschikbaar",
"contact_your_administrator_for_app_access": "Neem contact op met je beheerder om toegang te krijgen tot applicaties.",
"launch": "Lancering",
"client_launch_url": "URL voor lancering door klant",
"contact_your_administrator_for_app_access": "Neem contact op met de beheerder om toegang te krijgen tot applicaties.",
"launch": "Openen",
"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_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_description": "Toegang intrekken tot <b>{clientName}</b>. <b>{clientName}</b> kan je accountgegevens niet meer bekijken.",
"revoke_access_successful": "De toegang tot {clientName} is nu echt geblokkeerd."
"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 succesvol geblokkeerd.",
"last_signed_in_ago": "Laatst ingelogd {time} geleden",
"invalid_client_id": "De klant-ID mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.",
"custom_client_id_description": "Stel een aangepaste client-ID in als je app dit nodig heeft. Anders laat je het gewoon leeg en wordt er een willekeurige ID gegenereerd.",
"generated": "Gemaakt"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Kliknij, aby skopiować",
"something_went_wrong": "Coś poszło nie tak",
"go_back_to_home": "Wróć do strony głównej",
"dont_have_access_to_your_passkey": "Nie masz dostępu do swojego klucza?",
"alternative_sign_in_methods": "Alternatywne metody logowania",
"login_background": "Tło logowania",
"logo": "Logo",
"login_code": "Kod logowania",
@@ -276,6 +276,8 @@
"public_clients_description": "Klienci publiczni nie mają tajnego klucza. Są zaprojektowane dla aplikacji mobilnych, webowych i natywnych, gdzie tajne klucze nie mogą być bezpiecznie przechowywane.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Wymiana kodu publicznego klucza to funkcja zabezpieczająca, która zapobiega atakom CSRF i przechwytywaniu kodu autoryzacyjnego.",
"requires_reauthentication": "Wymagane ponowne uwierzytelnienie",
"requires_users_to_authenticate_again_on_each_authorization": "Wymaga od użytkowników ponownego uwierzytelniania przy każdej autoryzacji, nawet jeśli są już zalogowani.",
"name_logo": "Logo {name}",
"change_logo": "Zmień logo",
"upload_logo": "Prześlij logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Liczba przypadków, w których można użyć tokenu rejestracji.",
"expires": "Wygasają",
"signup": "Zarejestruj się",
"user_creation": "Tworzenie użytkowników",
"configure_user_creation": "Zarządzaj ustawieniami tworzenia użytkowników, w tym metodami rejestracji i domyślnymi uprawnieniami dla nowych użytkowników.",
"user_creation_groups_description": "Przypisz te grupy automatycznie nowym użytkownikom podczas rejestracji.",
"user_creation_claims_description": "Przypisuj te niestandardowe oświadczenia automatycznie nowym użytkownikom podczas rejestracji.",
"user_creation_updated_successfully": "Ustawienia tworzenia użytkowników zostały pomyślnie zaktualizowane.",
"signup_disabled_description": "Rejestracja użytkowników jest całkowicie wyłączona. Tylko administratorzy mogą tworzyć nowe konta użytkowników.",
"signup_requires_valid_token": "Aby utworzyć konto, wymagany jest ważny token rejestracyjny.",
"validating_signup_token": "Weryfikacja tokenu rejestracji",
"go_to_login": "Przejdź do logowania",
@@ -396,7 +404,7 @@
"skip_for_now": "Pomiń na razie",
"account_created": "Konto utworzone",
"enable_user_signups": "Włącz rejestrację użytkowników",
"enable_user_signups_description": "Czy funkcja rejestracji użytkowników powinna być włączona.",
"enable_user_signups_description": "Zdecyduj, w jaki sposób użytkownicy mogą rejestrować nowe konta w Pocket ID.",
"user_signups_are_disabled": "Rejestracja użytkowników jest obecnie wyłączona.",
"create_signup_token": "Utwórz token rejestracji",
"view_active_signup_tokens": "Wyświetl aktywne tokeny rejestracji",
@@ -412,7 +420,6 @@
"loading": "Ładowanie",
"delete_signup_token": "Usuń token rejestracji",
"are_you_sure_you_want_to_delete_this_signup_token": "Czy na pewno chcesz usunąć ten token rejestracji? Tego działania nie można cofnąć.",
"signup_disabled_description": "Rejestracja użytkowników jest całkowicie wyłączona. Tylko administratorzy mogą tworzyć nowe konta użytkowników.",
"signup_with_token": "Zarejestruj się za pomocą tokenu",
"signup_with_token_description": "Użytkownicy mogą zarejestrować się wyłącznie przy użyciu ważnego tokenu rejestracyjnego utworzonego przez administratora.",
"signup_open": "Otwórz rejestrację",
@@ -429,5 +436,9 @@
"client_name_description": "Nazwa klienta wyświetlana w interfejsie użytkownika Pocket ID.",
"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_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",
"invalid_client_id": "Identyfikator klienta może zawierać wyłącznie litery, cyfry, znaki podkreślenia i łączniki.",
"custom_client_id_description": "Ustaw niestandardowy identyfikator klienta, jeśli jest to wymagane przez twoją aplikację. W przeciwnym razie pozostaw to pole puste, aby wygenerować losowy identyfikator.",
"generated": "Wygenerowano"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Clique para copiar",
"something_went_wrong": "Algo deu errado",
"go_back_to_home": "Voltar para o início",
"dont_have_access_to_your_passkey": "Não tem acesso à sua chave de acesso?",
"alternative_sign_in_methods": "Outras formas de entrar",
"login_background": "Histórico de login",
"logo": "Logotipo",
"login_code": "Código de Login:",
@@ -276,6 +276,8 @@
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "A troca de chaves públicas é um recurso de segurança que evita ataques CSRF e interceptação de códigos de autorização.",
"requires_reauthentication": "Precisa autenticar de novo",
"requires_users_to_authenticate_again_on_each_authorization": "Pede que os usuários se autentiquem de novo em cada autorização, mesmo que já estejam conectados.",
"name_logo": "{name} logotipo",
"change_logo": "Alterar logotipo",
"upload_logo": "Carregar logotipo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Número de vezes que o token de inscrição pode ser usado.",
"expires": "Vence",
"signup": "Cadastre-se",
"user_creation": "Criação de usuário",
"configure_user_creation": "Gerencie as configurações de criação de usuários, incluindo métodos de inscrição e permissões padrão para novos usuários.",
"user_creation_groups_description": "Atribuir esses grupos automaticamente aos novos usuários quando eles se cadastrarem.",
"user_creation_claims_description": "Atribua essas reivindicações personalizadas automaticamente aos novos usuários no momento da inscrição.",
"user_creation_updated_successfully": "As configurações de criação do usuário foram atualizadas com sucesso.",
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.",
"signup_requires_valid_token": "É preciso um token de inscrição válido pra criar uma conta.",
"validating_signup_token": "Validando o token de inscrição",
"go_to_login": "Vá para o login",
@@ -396,7 +404,7 @@
"skip_for_now": "Pular por enquanto",
"account_created": "Conta criada",
"enable_user_signups": "Ativar inscrições de usuários",
"enable_user_signups_description": "Se a funcionalidade de cadastro de usuários deve ser ativada.",
"enable_user_signups_description": "Decida como os usuários podem se cadastrar para novas contas no Pocket ID.",
"user_signups_are_disabled": "As inscrições de usuários estão desativadas no momento.",
"create_signup_token": "Criar token de inscrição",
"view_active_signup_tokens": "Ver tokens de inscrição ativos",
@@ -412,7 +420,6 @@
"loading": "Carregando",
"delete_signup_token": "Apagar token de inscrição",
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que quer apagar esse token de inscrição? Não dá pra voltar atrás.",
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.",
"signup_with_token": "Cadastre-se com token",
"signup_with_token_description": "Os usuários só podem se cadastrar usando um token de cadastro válido criado por um administrador.",
"signup_open": "Inscrição aberta",
@@ -429,5 +436,9 @@
"client_name_description": "O nome do cliente que aparece na interface do Pocket ID.",
"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_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",
"invalid_client_id": "A ID do cliente só pode ter letras, números, sublinhados e hífens.",
"custom_client_id_description": "Defina um ID de cliente personalizado se for necessário para o seu aplicativo. Caso contrário, deixe em branco para gerar um aleatório.",
"generated": "Gerado"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Нажмите, чтобы скопировать",
"something_went_wrong": "Что-то пошло не так",
"go_back_to_home": "Вернуться на главную",
"dont_have_access_to_your_passkey": "Нет доступа к вашему пасскею?",
"alternative_sign_in_methods": "Альтернативные способы входа",
"login_background": "Фон страницы входа",
"logo": "Логотип",
"login_code": "Код входа",
@@ -112,7 +112,7 @@
"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": "Создайте одноразовый код входа, чтобы войти с другого устройства без passkey.",
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без пасскея.",
"create": "Создать",
"first_name": "Имя",
"last_name": "Фамилия",
@@ -276,6 +276,8 @@
"public_clients_description": "Публичные клиенты не имеют клиентского секрета. Они предназначены для мобильных, SPA и нативных приложений, где секретные данные нельзя надежно хранить.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
"requires_reauthentication": "Требуется повторная аутентификация",
"requires_users_to_authenticate_again_on_each_authorization": "Требует от пользователей повторной аутентификации при каждой авторизации, даже если они уже вошли в систему",
"name_logo": "Логотип {name}",
"change_logo": "Изменить логотип",
"upload_logo": "Загрузить логотип",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Количество раз, которое может быть использован токен регистрации.",
"expires": "Истекает",
"signup": "Зарегистрироваться",
"user_creation": "Создание пользователя",
"configure_user_creation": "Управляйте настройками создания пользователя, включая способы регистрации и разрешения по умолчанию для новых пользователей.",
"user_creation_groups_description": "Назначать эти группы автоматически новым пользователям при регистрации.",
"user_creation_claims_description": "Назначить эти пользовательские claims автоматически новым пользователям при регистрации.",
"user_creation_updated_successfully": "Настройки создания пользователя обновлены.",
"signup_disabled_description": "Регистрация пользователей полностью отключена. Только администраторы могут создавать новые учетные записи пользователей.",
"signup_requires_valid_token": "Для создания учетной записи необходим действительный токен регистрации",
"validating_signup_token": "Проверка токена регистрации",
"go_to_login": "Перейти ко входу",
@@ -396,7 +404,7 @@
"skip_for_now": "Пока пропустить",
"account_created": "Учетная запись создана",
"enable_user_signups": "Включить регистрацию пользователей",
"enable_user_signups_description": "Должна ли быть включена функция регистрации пользователя.",
"enable_user_signups_description": "Решите, как пользователи могут регистрировать новые учетные записи в Pocket ID.",
"user_signups_are_disabled": "Регистрация пользователей в настоящее время отключена",
"create_signup_token": "Создать токен регистрации",
"view_active_signup_tokens": "Показать активные токены регистрации",
@@ -412,22 +420,25 @@
"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": "Настоятельно рекомендуется настроить passkey, так как без него вы более не сможете войти в учетную запись после истечения сессии.",
"skip_passkey_setup_description": "Настоятельно рекомендуется настроить пасскей, так как без него вы более не сможете войти в учетную запись после истечения сессии.",
"my_apps": "Мои приложения",
"no_apps_available": "Нет доступных приложений",
"contact_your_administrator_for_app_access": "Свяжись с администратором, чтобы получить доступ к приложениям.",
"launch": "Запуск",
"client_launch_url": "URL запуска клиента",
"launch": "Запустить",
"client_launch_url": "Клиентский URL для запуска",
"client_launch_url_description": "URL-адрес, который откроется, когда кто-то запустит приложение со страницы «Мои приложения».",
"client_name_description": "Имя клиента, которое показывается в интерфейсе Pocket ID.",
"revoke_access": "Отменить доступ",
"revoke_access_description": "Отменить доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет заходить в твою учетную запись.",
"revoke_access_successful": "Доступ к {clientName} был успешно заблокирован."
"client_name_description": "Имя клиента, которое отображается в интерфейсе Pocket ID.",
"revoke_access": "Отозвать доступ",
"revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.",
"revoke_access_successful": "Доступ к {clientName} успешно отозван.",
"last_signed_in_ago": "Последний вход {time} назад",
"invalid_client_id": "ID клиента может содержать только буквы, цифры, подчеркивания и дефисы",
"custom_client_id_description": "Установите пользовательский ID клиента, если это нужно для вашего приложения. Если нет, оставьте поле пустым, чтобы он был сгенерирован случайным образом.",
"generated": "Сгенерированный"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Натисніть, щоб скопіювати",
"something_went_wrong": "Щось пішло не так",
"go_back_to_home": "Повернутися на головну сторінку",
"dont_have_access_to_your_passkey": "Не маєте доступу до свого ключа доступу?",
"alternative_sign_in_methods": "Альтернативні способи входу",
"login_background": "Фон сторінки входу",
"logo": "Логотип",
"login_code": "Код входу",
@@ -173,7 +173,7 @@
"smtp_tls_option": "Тип SMTP TLS",
"email_tls_option": "TLS налаштування електронної пошти",
"skip_certificate_verification": "Пропустити перевірку сертифіката",
"this_can_be_useful_for_selfsigned_certificates": "Ця опція може бути корисною для спопідписних сертифікатів.",
"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": "Надіслати електронний лист користувачеві після входу з нового пристрою.",
@@ -196,7 +196,7 @@
"client_configuration": "Налаштування клієнтів",
"ldap_url": "URL-адреса LDAP",
"ldap_bind_dn": "LDAP Bind DN",
"ldap_bind_password": "Пароль прив’язки LDAP",
"ldap_bind_password": "Пароль LDAP Bind",
"ldap_base_dn": "LDAP Base DN",
"user_search_filter": "Фільтр пошуку користувачів",
"the_search_filter_to_use_to_search_or_sync_users": "Фільтр пошуку для пошуку/синхронізації користувачів.",
@@ -251,8 +251,8 @@
"add_group": "Створити групу",
"manage_user_groups": "Керування групами користувачів",
"friendly_name": "Зручна назва",
"name_that_will_be_displayed_in_the_ui": "Ім'я, яке буде показуватися в інтерфейсі користувача",
"name_that_will_be_in_the_groups_claim": "Ім'я, яке буде в атрибуті \"groups\"",
"name_that_will_be_displayed_in_the_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": "Групу користувачів успішно видалено",
@@ -276,6 +276,8 @@
"public_clients_description": "Публічні клієнти не мають секретного ключа. Вони призначені для мобільних, веб та нативних додатків, де секретний ключ не може надійно зберігатись.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — це функція безпеки, що запобігає атакам типу CSRF та перехопленню коду авторизації.",
"requires_reauthentication": "Потрібна повторна автентифікація",
"requires_users_to_authenticate_again_on_each_authorization": "Вимагає від користувачів повторної автентифікації при кожній авторизації, навіть якщо вони вже ввійшли в систему.",
"name_logo": "Логотип {name}",
"change_logo": "Змінити логотип",
"upload_logo": "Вивантажити логотип",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Скільки разів можна використовувати реєстраційний токен.",
"expires": "Термін дії",
"signup": "Зареєструватися",
"user_creation": "Створення користувача",
"configure_user_creation": "Керуйте налаштуваннями створення користувачів, включаючи методи реєстрації та права доступу за замовчуванням для нових користувачів.",
"user_creation_groups_description": "Призначте ці групи автоматично новим користувачам під час реєстрації.",
"user_creation_claims_description": "Призначте ці власні вимоги автоматично новим користувачам під час реєстрації.",
"user_creation_updated_successfully": "Налаштування створення користувача успішно оновлено.",
"signup_disabled_description": "Реєстрація користувачів повністю вимкнена. Нові облікові записи можуть створювати лише адміністратори.",
"signup_requires_valid_token": "Для створення облікового запису потрібен дійсний токен реєстрації",
"validating_signup_token": "Перевірка токена реєстрації",
"go_to_login": "Перейти до входу",
@@ -396,7 +404,7 @@
"skip_for_now": "Пропустити наразі",
"account_created": "Обліковий запис створено",
"enable_user_signups": "Дозволити реєстрацію користувачів",
"enable_user_signups_description": "Чи слід увімкнути можливість реєстрації користувачів.",
"enable_user_signups_description": "Визначте, як користувачі можуть реєструвати нові облікові записи в Pocket ID.",
"user_signups_are_disabled": "Реєстрація користувачів наразі вимкнена",
"create_signup_token": "Створити токен реєстрації",
"view_active_signup_tokens": "Переглянути активні токени реєстрації",
@@ -412,7 +420,6 @@
"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": "Відкрита реєстрація",
@@ -420,14 +427,18 @@
"of": "з",
"skip_passkey_setup": "Пропустити налаштування ключа доступу",
"skip_passkey_setup_description": "Рекомендується налаштувати ключ доступу, оскільки без нього ви не зможете увійти у свій обліковий запис після закінчення сеансу.",
"my_apps": "Мої програми",
"my_apps": "Мої додатки",
"no_apps_available": "Немає доступних додатків",
"contact_your_administrator_for_app_access": "Зверніться до адміністратора, щоб отримати доступ до додатків.",
"launch": "Запуск",
"client_launch_url": "URL-адреса запуску клієнта",
"client_launch_url": "URL-адреса для запуску клієнта",
"client_launch_url_description": "URL-адреса, яка відкриється, коли користувач запустить програму зі сторінки «Мої програми».",
"client_name_description": "Ім'я клієнта, яке відображається в інтерфейсі Pocket ID.",
"client_name_description": "Назва клієнта, яке відображається в інтерфейсі Pocket ID.",
"revoke_access": "Скасувати доступ",
"revoke_access_description": "Скасувати доступ до <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.",
"revoke_access_successful": "Доступ до {clientName} було успішно скасовано."
"revoke_access_description": "Скасувати доступ для <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.",
"revoke_access_successful": "Доступ для {clientName} було успішно скасовано.",
"last_signed_in_ago": "Останній вхід {time} тому",
"invalid_client_id": "Ідентифікатор клієнта може містити тільки літери, цифри, підкреслення та дефіси.",
"custom_client_id_description": "Встановіть власний ідентифікатор клієнта, якщо це потрібно для вашої програми. В іншому випадку залиште поле порожнім, щоб створити випадковий ідентифікатор.",
"generated": "Створено"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "Nhấn để sao chép",
"something_went_wrong": "Đã xảy ra lỗi",
"go_back_to_home": "Quay lại trang chủ",
"dont_have_access_to_your_passkey": "Không có passkey?",
"alternative_sign_in_methods": "Các phương thức đăng nhập thay thế",
"login_background": "Hình nền đăng nhập",
"logo": "Logo",
"login_code": "Mã đăng nhập",
@@ -276,6 +276,8 @@
"public_clients_description": "Public client không có client secret. Chúng được thiết kế cho các ứng dụng di động, web và ứng dụng gốc, nơi các khóa bí mật không thể được lưu trữ một cách an toàn.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange là một tính năng bảo mật nhằm ngăn chặn các cuộc tấn công CSRF và đánh cắp mã xác thực.",
"requires_reauthentication": "Yêu cầu xác thực lại",
"requires_users_to_authenticate_again_on_each_authorization": "Yêu cầu người dùng xác thực lại mỗi lần ủy quyền, ngay cả khi đã đăng nhập.",
"name_logo": "{name} logo",
"change_logo": "Đổi Logo",
"upload_logo": "Tải logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "Số lần token đăng ký có thể được sử dụng.",
"expires": "Hết hạn",
"signup": "Đăng ký",
"user_creation": "Tạo tài khoản người dùng",
"configure_user_creation": "Quản lý cài đặt tạo tài khoản người dùng, bao gồm các phương thức đăng ký và quyền truy cập mặc định cho người dùng mới.",
"user_creation_groups_description": "Tự động gán các nhóm này cho người dùng mới khi họ đăng ký.",
"user_creation_claims_description": "Tự động gán các yêu cầu tùy chỉnh này cho người dùng mới khi họ đăng ký.",
"user_creation_updated_successfully": "Cài đặt tạo người dùng đã được cập nhật thành công.",
"signup_disabled_description": "Đăng ký người dùng đã bị vô hiệu hóa hoàn toàn. Chỉ quản trị viên mới có thể tạo tài khoản người dùng mới.",
"signup_requires_valid_token": "Yêu cầu mã đăng ký hợp lệ để tạo tài khoản",
"validating_signup_token": "Xác thực token đăng ký",
"go_to_login": "Đi tới đăng nhập",
@@ -396,7 +404,7 @@
"skip_for_now": "Tạm thời bỏ qua",
"account_created": "Tài khoản đã được tạo",
"enable_user_signups": "Bật đăng ký người dùng",
"enable_user_signups_description": "Có nên kích hoạt tính năng Đăng ký người dùng hay không.",
"enable_user_signups_description": "Quyết định cách người dùng có thể đăng ký tài khoản mới trong Pocket ID.",
"user_signups_are_disabled": "Đăng ký người dùng hiện đang bị vô hiệu hóa",
"create_signup_token": "Tạo Signup Token",
"view_active_signup_tokens": "Xem các Signup Tokens đang hoạt động",
@@ -412,7 +420,6 @@
"loading": "Đang tải",
"delete_signup_token": "Xóa mã Signup Token",
"are_you_sure_you_want_to_delete_this_signup_token": "Bạn có chắc muốn xoá signup token này không? Thao tác này không thể được hoàn lại.",
"signup_disabled_description": "Đăng ký người dùng đã bị vô hiệu hóa hoàn toàn. Chỉ quản trị viên mới có thể tạo tài khoản người dùng mới.",
"signup_with_token": "Đăng ký bằng token",
"signup_with_token_description": "Người dùng chỉ có thể đăng ký bằng mã đăng ký hợp lệ do quản trị viên tạo ra.",
"signup_open": "Mở Đăng Ký",
@@ -429,5 +436,9 @@
"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_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}",
"invalid_client_id": "ID khách hàng chỉ có thể chứa các ký tự chữ cái, số, dấu gạch dưới và dấu gạch ngang.",
"custom_client_id_description": "Đặt ID khách hàng tùy chỉnh nếu ứng dụng của bạn yêu cầu. Nếu không, hãy để trống để hệ thống tự động tạo một ID ngẫu nhiên.",
"generated": "Được tạo ra"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "点击复制",
"something_went_wrong": "出了点问题",
"go_back_to_home": "返回首页",
"dont_have_access_to_your_passkey": "无法使用您的通行密钥?试试其他登录方式",
"alternative_sign_in_methods": "替代登录方式",
"login_background": "登录页背景图",
"logo": "图标",
"login_code": "临时登录码",
@@ -276,6 +276,8 @@
"public_clients_description": "公共客户端没有客户端密钥。它们用于无法安全存储密钥的移动端、Web端和原生应用程序。",
"pkce": "公钥代码交换",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
"requires_reauthentication": "需要重新验证",
"requires_users_to_authenticate_again_on_each_authorization": "要求用户在每次授权时重新进行身份验证,即使用户已登录。",
"name_logo": "{name} Logo",
"change_logo": "更改 Logo",
"upload_logo": "上传 Logo",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "注册令牌最多使用次数。",
"expires": "过期时间",
"signup": "注册",
"user_creation": "用户创建",
"configure_user_creation": "管理用户创建设置,包括注册方式和新用户的默认权限。",
"user_creation_groups_description": "在用户注册时自动将这些组分配给新用户。",
"user_creation_claims_description": "在用户注册时自动为新用户分配这些自定义声明。",
"user_creation_updated_successfully": "用户创建设置已成功更新。",
"signup_disabled_description": "已完全禁止新用户注册。只有管理员可以创建新账户。",
"signup_requires_valid_token": "必须输入有效注册令牌才能注册新账户",
"validating_signup_token": "正在验证注册令牌",
"go_to_login": "跳转到登录界面",
@@ -396,7 +404,7 @@
"skip_for_now": "暂时跳过",
"account_created": "账户已创建",
"enable_user_signups": "允许用户注册",
"enable_user_signups_description": "是否启用新用户注册功能。",
"enable_user_signups_description": "决定用户如何在Pocket ID中注册新账户。",
"user_signups_are_disabled": "目前禁止新用户注册",
"create_signup_token": "创建一个注册令牌",
"view_active_signup_tokens": "查看有效注册令牌",
@@ -412,7 +420,6 @@
"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": "开放注册",
@@ -429,5 +436,9 @@
"client_name_description": "在Pocket ID用户界面中显示的客户端名称。",
"revoke_access": "撤销访问权限",
"revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。",
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。"
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。",
"last_signed_in_ago": "最后一次登录 {time} 前",
"invalid_client_id": "客户 ID 只能包含字母、数字、下划线和连字符。",
"custom_client_id_description": "如果您的应用程序需要自定义客户端 ID请在此处设置。否则请留空以生成一个随机生成的客户端 ID。",
"generated": "生成"
}

View File

@@ -23,7 +23,7 @@
"click_to_copy": "點擊以複製",
"something_went_wrong": "出了點問題",
"go_back_to_home": "返回首頁",
"dont_have_access_to_your_passkey": "無法存取您的密碼金鑰嗎?",
"alternative_sign_in_methods": "其他登入方法",
"login_background": "登入背景",
"logo": "標誌",
"login_code": "登入代碼",
@@ -276,6 +276,8 @@
"public_clients_description": "公開客戶端 (Public Client) 不包含 client secret。這類客戶端是為了行動裝置、網頁以及無法安全儲存 secret 的原生應用程式所設計。",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "PKCE公開金鑰碼交換是一項安全機制用於防止 CSRF 與授權碼攔截攻擊。",
"requires_reauthentication": "需要重新驗證",
"requires_users_to_authenticate_again_on_each_authorization": "要求使用者在每次授權時再次驗證,即使已經登入也是如此",
"name_logo": "{name} 標誌",
"change_logo": "更改標誌",
"upload_logo": "上傳標誌",
@@ -385,6 +387,12 @@
"number_of_times_token_can_be_used": "註冊令牌可使用的次數。",
"expires": "到期",
"signup": "註冊",
"user_creation": "使用者建立",
"configure_user_creation": "管理使用者建立設定,包括註冊方法和新使用者的預設權限。",
"user_creation_groups_description": "在新使用者註冊時自動指定這些群組。",
"user_creation_claims_description": "在新使用者註冊時自動將這些自訂索賠指派給他們。",
"user_creation_updated_successfully": "使用者建立設定更新成功。",
"signup_disabled_description": "使用者註冊已完全停用。只有管理員可以建立新的使用者帳號。",
"signup_requires_valid_token": "建立帳號需要有效的註冊令牌",
"validating_signup_token": "驗證註冊標記",
"go_to_login": "前往登入",
@@ -396,7 +404,7 @@
"skip_for_now": "暫時跳過",
"account_created": "帳號已建立",
"enable_user_signups": "啟用使用者註冊",
"enable_user_signups_description": "是否要啟用使用者註冊功能。",
"enable_user_signups_description": "決定使用者如何在 Pocket ID 中註冊新帳戶。",
"user_signups_are_disabled": "使用者註冊目前已停用",
"create_signup_token": "建立註冊令牌",
"view_active_signup_tokens": "檢視有效的註冊令牌",
@@ -412,7 +420,6 @@
"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": "開放報名",
@@ -429,5 +436,9 @@
"client_name_description": "顯示在 Pocket ID UI 中的用戶端名稱。",
"revoke_access": "撤銷存取權",
"revoke_access_description": "撤銷存取 <b>{clientName}</b>. <b>{clientName}</b>將無法再存取您的帳戶資訊。",
"revoke_access_successful": "{clientName} 的存取權已成功取消。"
"revoke_access_successful": "{clientName} 的存取權已成功取消。",
"last_signed_in_ago": "上次登入 {time} 前",
"invalid_client_id": "用戶端 ID 只能包含字母、數字、底線和連字符",
"custom_client_id_description": "如果您的應用程式需要,請設定自訂用戶端 ID。否則請留空以產生隨機 ID。",
"generated": "產生"
}

View File

@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "1.7.0",
"version": "1.8.1",
"private": true,
"type": "module",
"scripts": {
@@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.11.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"jose": "^5.10.0",
"qrcode": "^1.5.4",
"sveltekit-superforms": "^2.27.1",
@@ -46,6 +47,7 @@
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"rollup": "^4.46.3",
"svelte": "^5.36.16",
"svelte-check": "^4.3.0",
"svelte-sonner": "^1.0.5",

View File

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

View File

@@ -132,6 +132,7 @@
/* Font */
--font-playfair: 'Playfair Display', serif;
--font-code: 'Google Sans', sans-serif;
}
@layer base {
@@ -167,6 +168,11 @@
font-weight: 700;
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 {

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"
>
{m.dont_have_access_to_your_passkey()}
{m.alternative_sign_in_methods()}
</a>
</div>
{/if}
@@ -82,7 +82,7 @@
)}`}
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>
{/if}
</Card.CardContent>

View File

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

View File

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

View File

@@ -14,10 +14,15 @@ export default class AppConfigService extends APIService {
}
async update(appConfig: AllAppConfig) {
// Convert all values to string
const appConfigConvertedToString = {};
// Convert all values to string, stringifying JSON where needed
const appConfigConvertedToString: Record<string, string> = {};
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);
return this.parseConfigList(res.data);
@@ -66,6 +71,16 @@ export default class AppConfigService extends APIService {
}
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') {
return true;
} else if (value === 'false') {

View File

@@ -1,9 +1,10 @@
import type {
AuthorizedOidcClient,
AccessibleOidcClient,
AuthorizeResponse,
OidcClient,
OidcClientCreate,
OidcClientMetaData,
OidcClientUpdate,
OidcClientWithAllowedUserGroups,
OidcClientWithAllowedUserGroupsCount,
OidcDeviceCodeInfo
@@ -19,7 +20,8 @@ class OidcService extends APIService {
callbackURL: string,
nonce?: string,
codeChallenge?: string,
codeChallengeMethod?: string
codeChallengeMethod?: string,
reauthenticationToken?: string
) {
const res = await this.api.post('/oidc/authorize', {
scope,
@@ -27,7 +29,8 @@ class OidcService extends APIService {
callbackURL,
clientId,
codeChallenge,
codeChallengeMethod
codeChallengeMethod,
reauthenticationToken
});
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;
}
async updateClient(id: string, client: OidcClientCreate) {
async updateClient(id: string, client: OidcClientUpdate) {
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
}
@@ -115,22 +118,16 @@ class OidcService extends APIService {
return response.data;
}
async listAuthorizedClients(options?: SearchPaginationSortRequest) {
async listOwnAccessibleClients(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/oidc/users/me/clients', {
params: options
});
return res.data as Paginated<AuthorizedOidcClient>;
}
async listAuthorizedClientsForUser(userId: string, options?: SearchPaginationSortRequest) {
const res = await this.api.get(`/oidc/users/${userId}/clients`, {
params: options
});
return res.data as Paginated<AuthorizedOidcClient>;
return res.data as Paginated<AccessibleOidcClient>;
}
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);
}
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`, {
userId,
expiresAt
ttl,
});
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`, {
expiresAt,
ttl,
usageLimit
});
return res.data.token;
@@ -100,8 +100,8 @@ export default class UserService extends APIService {
await this.api.post('/one-time-access-email', { email, redirectPath });
}
async requestOneTimeAccessEmailAsAdmin(userId: string, expiresAt: Date) {
await this.api.post(`/users/${userId}/one-time-access-email`, { expiresAt });
async requestOneTimeAccessEmailAsAdmin(userId: string, ttl: string|number) {
await this.api.post(`/users/${userId}/one-time-access-email`, { ttl });
}
async updateUserGroups(id: string, userGroupIds: string[]) {

View File

@@ -37,6 +37,11 @@ class WebAuthnService extends APIService {
async updateCredentialName(id: string, name: string) {
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;

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,26 @@
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
import { setDefaultOptions } from 'date-fns';
import { z } from 'zod/v4';
export function setLocale(locale: Locale, reload = true) {
import(`../../../node_modules/zod/v4/locales/${locale}.js`)
.then((zodLocale) => z.config(zodLocale.default()))
.finally(() => {
setParaglideLocale(locale, { reload });
export async function setLocale(locale: Locale, reload = true) {
const [zodResult, dateFnsResult] = await Promise.allSettled([
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
import(`../../../node_modules/date-fns/locale/${locale}.js`)
]);
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';
export const optionalString = z
.string()
.transform((v) => (v === '' ? undefined : v))
.optional();
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
z.preprocess((v) => (v === '' ? undefined : v), validation);
export const optionalUrl = z
.url()

View File

@@ -6,8 +6,6 @@
import Header from '$lib/components/header/header.svelte';
import { Toaster } from '$lib/components/ui/sonner';
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 { ModeWatcher } from 'mode-watcher';
import type { Snippet } from 'svelte';
@@ -28,14 +26,6 @@
if (redirectPath) {
goto(redirectPath);
}
if (user) {
userStore.setUser(user);
}
if (appConfig) {
appConfigStore.set(appConfig);
}
</script>
{#if !appConfig}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
import type { ApiKeyCreate } from '$lib/types/api-key.type';
import { preventDefault } from '$lib/utils/event-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';
let {
@@ -28,7 +28,7 @@
const formSchema = z.object({
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())
});

View File

@@ -5,11 +5,12 @@
import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration';
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 AppConfigEmailForm from './forms/app-config-email-form.svelte';
import AppConfigGeneralForm from './forms/app-config-general-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';
let { data } = $props();
@@ -68,6 +69,17 @@
</CollapsibleCard>
</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>
<CollapsibleCard
id="application-configuration-email"

View File

@@ -23,27 +23,11 @@
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 = {
appName: appConfig.appName,
sessionDuration: appConfig.sessionDuration,
emailsVerified: appConfig.emailsVerified,
allowOwnAccountEdit: appConfig.allowOwnAccountEdit,
allowUserSignups: appConfig.allowUserSignups,
disableAnimations: appConfig.disableAnimations,
accentColor: appConfig.accentColor
};
@@ -53,7 +37,6 @@
sessionDuration: z.number().min(1).max(43200),
emailsVerified: z.boolean(),
allowOwnAccountEdit: z.boolean(),
allowUserSignups: z.enum(['disabled', 'withToken', 'open']),
disableAnimations: z.boolean(),
accentColor: z.string()
});
@@ -80,55 +63,6 @@
description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()}
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
id="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}
<div transition:slide>
<Card.Content>
<OIDCClientForm callback={createOIDCClient} />
<OIDCClientForm mode="create" callback={createOIDCClient} />
</Card.Content>
</div>
{/if}

View File

@@ -36,7 +36,8 @@
[m.userinfo_url()]: `https://${page.url.hostname}/api/oidc/userinfo`,
[m.logout_url()]: `https://${page.url.hostname}/api/oidc/end-session`,
[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) {
@@ -49,6 +50,9 @@
client.isPublic = updatedClient.isPublic;
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
setupDetails[m.requires_reauthentication()] = updatedClient.requiresReauthentication
? m.enabled()
: m.disabled();
await Promise.all([dataPromise, imagePromise])
.then(() => {
@@ -120,14 +124,14 @@
<Card.Content>
<div class="flex flex-col">
<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}>
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
</CopyToClipboard>
</div>
{#if !client.isPublic}
<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}
<CopyToClipboard value={$clientSecretStore}>
<span class="text-muted-foreground text-sm" data-testid="client-secret">
@@ -154,7 +158,7 @@
<div transition:slide>
{#each Object.entries(setupDetails) as [key, value]}
<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}>
<span class="text-muted-foreground text-sm">{value}</span>
</CopyToClipboard>
@@ -175,7 +179,7 @@
</Card.Root>
<Card.Root>
<Card.Content>
<OidcForm existingClient={client} callback={updateClient} />
<OidcForm mode="update" existingClient={client} callback={updateClient} />
</Card.Content>
</Card.Root>
<CollapsibleCard

View File

@@ -6,24 +6,30 @@
import { Button } from '$lib/components/ui/button';
import Label from '$lib/components/ui/label/label.svelte';
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 { preventDefault } from '$lib/utils/event-util';
import { createForm } from '$lib/utils/form-util';
import { cn } from '$lib/utils/style';
import { emptyToUndefined, optionalUrl } from '$lib/utils/zod-util';
import { LucideChevronDown } from '@lucide/svelte';
import { slide } from 'svelte/transition';
import { z } from 'zod/v4';
import FederatedIdentitiesInput from './federated-identities-input.svelte';
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
import { optionalUrl } from '$lib/utils/zod-util';
let {
callback,
existingClient
existingClient,
mode
}: {
existingClient?: OidcClient;
callback: (user: OidcClientCreateWithLogo) => Promise<boolean>;
callback: (client: OidcClientCreateWithLogo | OidcClientUpdateWithLogo) => Promise<boolean>;
mode: 'create' | 'update';
} = $props();
let isLoading = $state(false);
@@ -34,11 +40,13 @@
);
const client = {
id: '',
name: existingClient?.name || '',
callbackURLs: existingClient?.callbackURLs || [],
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
isPublic: existingClient?.isPublic || false,
pkceEnabled: existingClient?.pkceEnabled || false,
requiresReauthentication: existingClient?.requiresReauthentication || false,
launchURL: existingClient?.launchURL || '',
credentials: {
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
@@ -46,11 +54,22 @@
};
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),
callbackURLs: z.array(z.string().nonempty()).default([]),
logoutCallbackURLs: z.array(z.string().nonempty()),
isPublic: z.boolean(),
pkceEnabled: z.boolean(),
requiresReauthentication: z.boolean(),
launchURL: optionalUrl,
credentials: z.object({
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()}
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 class="mt-8">
<Label for="logo">{m.logo()}</Label>
@@ -177,7 +202,16 @@
</div>
{#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
client={existingClient}
bind:federatedIdentities={$inputs.credentials.value.federatedIdentities}
@@ -189,7 +223,7 @@
<div class="relative mt-5 flex justify-center">
<Button
variant="ghost"
class="text-muted-foregroun"
class="text-muted-foreground"
onclick={() => (showAdvancedOptions = !showAdvancedOptions)}
>
{showAdvancedOptions ? m.hide_advanced_options() : m.show_advanced_options()}

View File

@@ -3,25 +3,25 @@
import * as Pagination from '$lib/components/ui/pagination';
import { m } from '$lib/paraglide/messages';
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 { axiosErrorToast } from '$lib/utils/error-util';
import { LayoutDashboard } from '@lucide/svelte';
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 authorizedClients: Paginated<AuthorizedOidcClient> = $state(data.authorizedClients);
let clients: Paginated<AccessibleOidcClient> = $state(data.clients);
let requestOptions: SearchPaginationSortRequest = $state(data.appRequestOptions);
const oidcService = new OIDCService();
async function onRefresh(options: SearchPaginationSortRequest) {
authorizedClients = await oidcService.listAuthorizedClients(options);
clients = await oidcService.listOwnAccessibleClients(options);
}
async function onPageChange(page: number) {
requestOptions.pagination = { limit: authorizedClients.pagination.itemsPerPage, page };
requestOptions.pagination = { limit: clients.pagination.itemsPerPage, page };
onRefresh(requestOptions);
}
@@ -64,7 +64,7 @@
</h1>
</div>
{#if authorizedClients.data.length === 0}
{#if clients.data.length === 0}
<div class="py-16 text-center">
<LayoutDashboard class="text-muted-foreground mx-auto mb-4 size-16" />
<h3 class="text-muted-foreground mb-2 text-lg font-medium">
@@ -76,20 +76,23 @@
</div>
{:else}
<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">
{#each authorizedClients.data as authorizedClient}
<AuthorizedOidcClientCard {authorizedClient} onRevoke={revokeAuthorizedClient} />
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));"
>
{#each clients.data as client}
<AuthorizedOidcClientCard {client} onRevoke={revokeAuthorizedClient} />
{/each}
</div>
{#if authorizedClients.pagination.totalPages > 1}
{#if clients.pagination.totalPages > 1}
<div class="border-border flex items-center justify-center border-t pt-3">
<Pagination.Root
class="mx-0 w-auto"
count={authorizedClients.pagination.totalItems}
perPage={authorizedClients.pagination.itemsPerPage}
count={clients.pagination.totalItems}
perPage={clients.pagination.itemsPerPage}
{onPageChange}
page={authorizedClients.pagination.currentPage}
page={clients.pagination.currentPage}
>
{#snippet children({ pages })}
<Pagination.Content class="flex justify-center">
@@ -101,7 +104,7 @@
<Pagination.Item>
<Pagination.Link
{page}
isActive={authorizedClients.pagination.currentPage === page.value}
isActive={clients.pagination.currentPage === page.value}
>
{page.value}
</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 * as Card from '$lib/components/ui/card';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { m } from '$lib/paraglide/messages';
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 {
LucideBan,
LucideEllipsisVertical,
LucideExternalLink,
LucideLogIn,
LucidePencil
} from '@lucide/svelte';
import { formatDistanceToNow } from 'date-fns';
import { mode } from 'mode-watcher';
let {
authorizedClient,
client,
onRevoke
}: {
authorizedClient: AuthorizedOidcClient;
client: AccessibleOidcClient;
onRevoke: (client: OidcClientMetaData) => Promise<void>;
} = $props();
@@ -28,7 +31,7 @@
</script>
<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"
>
<Card.Content class=" p-0">
@@ -36,60 +39,84 @@
<div class="aspect-square h-[56px]">
<ImageBox
class="grow rounded-lg object-contain"
src={authorizedClient.client.hasLogo
? cachedOidcClientLogo.getUrl(authorizedClient.client.id)
src={client.hasLogo
? cachedOidcClientLogo.getUrl(client.id)
: cachedApplicationLogo.getUrl(isLightMode)}
alt={m.name_logo({ name: authorizedClient.client.name })}
alt={m.name_logo({ name: client.name })}
/>
</div>
<div class="flex w-full justify-between gap-3">
<div>
<div class="mb-1 flex items-start gap-2">
<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>
</div>
{#if authorizedClient.client.launchURL}
{#if client.launchURL}
<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>
{/if}
</div>
<div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<LucideEllipsisVertical class="size-4" />
<span class="sr-only">{m.toggle_menu()}</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item
onclick={() => goto(`/settings/admin/oidc-clients/${authorizedClient.client.id}`)}
><LucidePencil class="mr-2 size-4" /> {m.edit()}</DropdownMenu.Item
>
{#if $userStore?.isAdmin}
<DropdownMenu.Item
class="text-red-500 focus:!text-red-700"
onclick={() => onRevoke(authorizedClient.client)}
><LucideBan class="mr-2 size-4" />{m.revoke()}</DropdownMenu.Item
>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{#if $userStore?.isAdmin || client.lastUsedAt}
<div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<LucideEllipsisVertical class="size-4" />
<span class="sr-only">{m.toggle_menu()}</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#if $userStore?.isAdmin}
<DropdownMenu.Item
onclick={() => goto(`/settings/admin/oidc-clients/${client.id}`)}
><LucidePencil class="mr-2 size-4" /> {m.edit()}</DropdownMenu.Item
>
{/if}
{#if client.lastUsedAt}
<DropdownMenu.Item
class="text-red-500 focus:!text-red-700"
onclick={() => onRevoke(client)}
><LucideBan class="mr-2 size-4" />{m.revoke()}</DropdownMenu.Item
>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/if}
</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
href={authorizedClient.client.launchURL}
href={client.launchURL}
target="_blank"
size="sm"
class="h-8 text-xs"
disabled={!authorizedClient.client.launchURL}
rel="noopener noreferrer"
disabled={!client.launchURL}
>
{m.launch()}
<LucideExternalLink class="ml-1 size-3" />

View File

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

View File

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

Binary file not shown.

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