mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-27 23:43:52 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c54e2e6b0 | ||
|
|
a5efb95065 | ||
|
|
625f235740 | ||
|
|
2c122d413d | ||
|
|
fc0c99a232 | ||
|
|
24e274200f | ||
|
|
0aab3f3c7a | ||
|
|
182d809028 | ||
|
|
c51265dafb | ||
|
|
0cb039d35d | ||
|
|
7ab0fd3028 | ||
|
|
49f0fa423c | ||
|
|
61e63e411d | ||
|
|
9339e88a5a | ||
|
|
fe003b927c | ||
|
|
f5b5b1bd85 | ||
|
|
d28bfac81f | ||
|
|
b04e3e8ecf | ||
|
|
d77d8eb068 | ||
|
|
7cd88aca25 | ||
|
|
b5e6371eaa | ||
|
|
544b98c1d0 | ||
|
|
3188e92257 | ||
|
|
3fa2f9a162 | ||
|
|
7b1f6b8857 | ||
|
|
17d8893bdb | ||
|
|
0e44f245af | ||
|
|
824e8f1a0f |
4
.github/workflows/backend-linter.yml
vendored
4
.github/workflows/backend-linter.yml
vendored
@@ -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' }}
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.7.0...v) (2025-08-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option to OIDC client to require re-authentication ([#747](https://github.com/pocket-id/pocket-id/issues/747)) ([0cb039d](https://github.com/pocket-id/pocket-id/commit/0cb039d35d49206011064e622f3bfd3d8f88720f))
|
||||
* allow custom client IDs ([#864](https://github.com/pocket-id/pocket-id/issues/864)) ([a5efb95](https://github.com/pocket-id/pocket-id/commit/a5efb9506582884c70b9b1fd737ebdd44b101b47))
|
||||
* display all accessible oidc clients in the dashboard ([#832](https://github.com/pocket-id/pocket-id/issues/832)) ([3188e92](https://github.com/pocket-id/pocket-id/commit/3188e92257afcaf7a16dd418e4c40626d7e1d034))
|
||||
* login code font change ([#851](https://github.com/pocket-id/pocket-id/issues/851)) ([d28bfac](https://github.com/pocket-id/pocket-id/commit/d28bfac81fc24ee79e4896538a616f0a89ab30a5))
|
||||
* **signup:** add default user groups and claims for new users ([#812](https://github.com/pocket-id/pocket-id/issues/812)) ([182d809](https://github.com/pocket-id/pocket-id/commit/182d8090286f9953171c6c410283be679889aca7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* authorization can't be revoked ([0aab3f3](https://github.com/pocket-id/pocket-id/commit/0aab3f3c7ad8c1b14939de3ded60c9f201eab8fc))
|
||||
* delete webauthn session after login to prevent replay attacks ([fe003b9](https://github.com/pocket-id/pocket-id/commit/fe003b927ce7772692439992860c804de89ce424))
|
||||
* **deps:** bump rollup from 4.45.3 to 4.46.3 ([#845](https://github.com/pocket-id/pocket-id/issues/845)) ([b5e6371](https://github.com/pocket-id/pocket-id/commit/b5e6371eaaf3d9e85d8b05c457487c4425fa8381))
|
||||
* enable foreign key check for sqlite ([#863](https://github.com/pocket-id/pocket-id/issues/863)) ([625f235](https://github.com/pocket-id/pocket-id/commit/625f23574001ebd7074b8d98d448a2811847be16))
|
||||
* ferated identities can't be cleared ([24e2742](https://github.com/pocket-id/pocket-id/commit/24e274200fe4002d01c58cc3fa74094b598d7599))
|
||||
* for one-time access tokens and signup tokens, pass TTLs instead of absolute expiration date ([#855](https://github.com/pocket-id/pocket-id/issues/855)) ([7ab0fd3](https://github.com/pocket-id/pocket-id/commit/7ab0fd30286e6b67b5ce586484d82a20c42b471d))
|
||||
* ignore client secret if client is public ([#836](https://github.com/pocket-id/pocket-id/issues/836)) ([7b1f6b8](https://github.com/pocket-id/pocket-id/commit/7b1f6b88572bac1f3e838a9e904917fbd5fbdf61))
|
||||
* move audit log call before TX is committed ([#854](https://github.com/pocket-id/pocket-id/issues/854)) ([9339e88](https://github.com/pocket-id/pocket-id/commit/9339e88a5a26ff77a5e40149cbb1a5b339b7ec6a))
|
||||
* non admin users can't revoke oidc client but see edit link ([0e44f24](https://github.com/pocket-id/pocket-id/commit/0e44f245afcdf8179bf619613ca9ef4bffa176ca))
|
||||
* oidc client advanced options color ([fc0c99a](https://github.com/pocket-id/pocket-id/commit/fc0c99a232b0efb1a5b5d2c551102418b1080293))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.6.4...v) (2025-08-10)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
181
backend/go.sum
181
backend/go.sum
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
42
backend/internal/utils/json_util.go
Normal file
42
backend/internal/utils/json_util.go
Normal 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")
|
||||
}
|
||||
}
|
||||
64
backend/internal/utils/json_util_test.go
Normal file
64
backend/internal/utils/json_util_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN requires_reauthentication;
|
||||
DROP TABLE IF EXISTS reauthentication_tokens;
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE TABLE reauthentication_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op
|
||||
@@ -0,0 +1,173 @@
|
||||
---------------------------
|
||||
-- Delete all orphaned rows
|
||||
---------------------------
|
||||
UPDATE oidc_clients
|
||||
SET created_by_id = NULL
|
||||
WHERE created_by_id IS NOT NULL
|
||||
AND created_by_id NOT IN (SELECT id FROM users);
|
||||
|
||||
DELETE FROM oidc_authorization_codes WHERE user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM one_time_access_tokens WHERE user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM webauthn_credentials WHERE user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM audit_logs WHERE user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM api_keys WHERE user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users);
|
||||
|
||||
DELETE FROM oidc_refresh_tokens WHERE user_id NOT IN (SELECT id FROM users) OR client_id NOT IN (SELECT id FROM oidc_clients);
|
||||
DELETE FROM oidc_device_codes WHERE (user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users)) OR client_id NOT IN (SELECT id FROM oidc_clients);
|
||||
DELETE FROM user_authorized_oidc_clients WHERE user_id NOT IN (SELECT id FROM users) OR client_id NOT IN (SELECT id FROM oidc_clients);
|
||||
|
||||
DELETE FROM user_groups_users WHERE user_id NOT IN (SELECT id FROM users) OR user_group_id NOT IN (SELECT id FROM user_groups);
|
||||
|
||||
DELETE FROM custom_claims WHERE (user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users)) OR (user_group_id IS NOT NULL AND user_group_id NOT IN (SELECT id FROM user_groups));
|
||||
|
||||
DELETE FROM oidc_clients_allowed_user_groups WHERE oidc_client_id NOT IN (SELECT id FROM oidc_clients) OR user_group_id NOT IN (SELECT id FROM user_groups);
|
||||
|
||||
DELETE FROM reauthentication_tokens WHERE user_id NOT IN (SELECT id FROM users);
|
||||
|
||||
---------------------------
|
||||
-- Add missing foreign keys and edit cascade behavior where necessary
|
||||
---------------------------
|
||||
|
||||
-- reauthentication_tokens: add missing FK user_id → users
|
||||
CREATE TABLE reauthentication_tokens_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO reauthentication_tokens_new (id, created_at, token, expires_at, user_id)
|
||||
SELECT id, created_at, token, expires_at, user_id
|
||||
FROM reauthentication_tokens;
|
||||
DROP TABLE reauthentication_tokens;
|
||||
ALTER TABLE reauthentication_tokens_new RENAME TO reauthentication_tokens;
|
||||
CREATE INDEX idx_reauthentication_tokens_token
|
||||
ON reauthentication_tokens (token);
|
||||
|
||||
-- oidc_authorization_codes: add FK client_id, user_id → CASCADE
|
||||
CREATE TABLE oidc_authorization_codes_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
scope TEXT NOT NULL,
|
||||
nonce TEXT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
|
||||
code_challenge TEXT,
|
||||
code_challenge_method_sha256 NUMERIC
|
||||
);
|
||||
INSERT INTO oidc_authorization_codes_new
|
||||
(id, created_at, code, scope, nonce, expires_at, user_id, client_id, code_challenge, code_challenge_method_sha256)
|
||||
SELECT id, created_at, code, scope, nonce, expires_at, user_id, client_id, code_challenge, code_challenge_method_sha256
|
||||
FROM oidc_authorization_codes;
|
||||
DROP TABLE oidc_authorization_codes;
|
||||
ALTER TABLE oidc_authorization_codes_new RENAME TO oidc_authorization_codes;
|
||||
|
||||
-- user_authorized_oidc_clients: add FK user_id, cascade client_id
|
||||
CREATE TABLE user_authorized_oidc_clients_new
|
||||
(
|
||||
scope TEXT,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
|
||||
last_used_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, client_id)
|
||||
);
|
||||
INSERT INTO user_authorized_oidc_clients_new (scope, user_id, client_id, last_used_at)
|
||||
SELECT scope, user_id, client_id, last_used_at
|
||||
FROM user_authorized_oidc_clients;
|
||||
DROP TABLE user_authorized_oidc_clients;
|
||||
ALTER TABLE user_authorized_oidc_clients_new RENAME TO user_authorized_oidc_clients;
|
||||
|
||||
-- audit_logs: user_id → CASCADE
|
||||
CREATE TABLE audit_logs_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
country TEXT,
|
||||
city TEXT
|
||||
);
|
||||
INSERT INTO audit_logs_new
|
||||
(id, created_at, event, ip_address, user_agent, data, user_id, country, city)
|
||||
SELECT id, created_at, event, ip_address, user_agent, data, user_id, country, city
|
||||
FROM audit_logs;
|
||||
DROP TABLE audit_logs;
|
||||
ALTER TABLE audit_logs_new RENAME TO audit_logs;
|
||||
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
|
||||
CREATE INDEX idx_audit_logs_country ON audit_logs (country);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);
|
||||
CREATE INDEX idx_audit_logs_event ON audit_logs (event);
|
||||
CREATE INDEX idx_audit_logs_user_agent ON audit_logs (user_agent);
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
|
||||
|
||||
-- oidc_clients: created_by_id → SET NULL
|
||||
CREATE TABLE oidc_clients_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT,
|
||||
secret TEXT,
|
||||
callback_urls BLOB,
|
||||
image_type TEXT,
|
||||
created_by_id TEXT REFERENCES users ON DELETE SET NULL,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
pkce_enabled BOOLEAN DEFAULT FALSE,
|
||||
logout_callback_urls BLOB,
|
||||
credentials TEXT,
|
||||
launch_url TEXT,
|
||||
requires_reauthentication BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
INSERT INTO oidc_clients_new
|
||||
(id, created_at, name, secret, callback_urls, image_type, created_by_id,
|
||||
is_public, pkce_enabled, logout_callback_urls, credentials, launch_url, requires_reauthentication)
|
||||
SELECT id, created_at, name, secret, callback_urls, image_type, created_by_id,
|
||||
is_public, pkce_enabled, logout_callback_urls, credentials, launch_url, requires_reauthentication
|
||||
FROM oidc_clients;
|
||||
DROP TABLE oidc_clients;
|
||||
ALTER TABLE oidc_clients_new RENAME TO oidc_clients;
|
||||
|
||||
-- one_time_access_tokens: user_id → CASCADE
|
||||
CREATE TABLE one_time_access_tokens_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO one_time_access_tokens_new
|
||||
(id, created_at, token, expires_at, user_id)
|
||||
SELECT id, created_at, token, expires_at, user_id
|
||||
FROM one_time_access_tokens;
|
||||
DROP TABLE one_time_access_tokens;
|
||||
ALTER TABLE one_time_access_tokens_new RENAME TO one_time_access_tokens;
|
||||
|
||||
-- webauthn_credentials: user_id → CASCADE
|
||||
CREATE TABLE webauthn_credentials_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
attestation_type TEXT NOT NULL,
|
||||
transport BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
backup_eligible BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
backup_state BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
INSERT INTO webauthn_credentials_new
|
||||
(id, created_at, name, credential_id, public_key, attestation_type,
|
||||
transport, user_id, backup_eligible, backup_state)
|
||||
SELECT id, created_at, name, credential_id, public_key, attestation_type,
|
||||
transport, user_id, backup_eligible, backup_state
|
||||
FROM webauthn_credentials;
|
||||
DROP TABLE webauthn_credentials;
|
||||
ALTER TABLE webauthn_credentials_new RENAME TO webauthn_credentials;
|
||||
@@ -429,5 +429,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -429,5 +431,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -385,6 +385,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 +418,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 +434,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
434
frontend/messages/ko.json
Normal file
434
frontend/messages/ko.json
Normal file
@@ -0,0 +1,434 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "내 계정",
|
||||
"logout": "로그아웃",
|
||||
"confirm": "확인",
|
||||
"docs": "문서",
|
||||
"key": "키",
|
||||
"value": "값",
|
||||
"remove_custom_claim": "사용자 정의 클레임 제거",
|
||||
"add_custom_claim": "사용자 정의 클레임 추가",
|
||||
"add_another": "추가",
|
||||
"select_a_date": "날짜 선택",
|
||||
"select_file": "파일 선택",
|
||||
"profile_picture": "프로필 사진",
|
||||
"profile_picture_is_managed_by_ldap_server": "프로필 사진이 LDAP 서버에서 관리되어 여기에서 변경할 수 없습니다.",
|
||||
"click_profile_picture_to_upload_custom": "프로필 사진을 클릭하여 파일에서 사용자 정의 사진을 업로드하세요.",
|
||||
"image_should_be_in_format": "이미지는 PNG 또는 JPEG 형식이여야 합니다.",
|
||||
"items_per_page": "페이지당 항목",
|
||||
"no_items_found": "항목 없음",
|
||||
"search": "검색...",
|
||||
"expand_card": "카드 확장",
|
||||
"copied": "복사됨",
|
||||
"click_to_copy": "클릭하여 복사",
|
||||
"something_went_wrong": "문제가 발생했습니다",
|
||||
"go_back_to_home": "홈으로 돌아가기",
|
||||
"dont_have_access_to_your_passkey": "패스키에 접근할 수 없나요?",
|
||||
"login_background": "로그인 배경",
|
||||
"logo": "로고",
|
||||
"login_code": "로그인 코드",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "패스키 없이 한 번 로그인 할 수 있는 로그인 코드를 생성합니다.",
|
||||
"one_hour": "1시간",
|
||||
"twelve_hours": "12시간",
|
||||
"one_day": "1일",
|
||||
"one_week": "1주",
|
||||
"one_month": "1달",
|
||||
"expiration": "만료",
|
||||
"generate_code": "코드 생성",
|
||||
"name": "이름",
|
||||
"browser_unsupported": "지원되지 않는 브라우저",
|
||||
"this_browser_does_not_support_passkeys": "이 브라우저는 패스키를 지원하지 않습니다. 다른 로그인 방법을 사용하세요.",
|
||||
"an_unknown_error_occurred": "알 수 없는 오류 발생",
|
||||
"authentication_process_was_aborted": "인증 프로세스가 중단되었습니다",
|
||||
"error_occurred_with_authenticator": "인증기에서 오류가 발생했습니다",
|
||||
"authenticator_does_not_support_discoverable_credentials": "인증기가 발견 가능한 자격 증명을 지원하지 않습니다",
|
||||
"authenticator_does_not_support_resident_keys": "인증기가 레지던트 키를 지원하지 않습니다",
|
||||
"passkey_was_previously_registered": "이 패스키는 이미 등록되었습니다",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "인증기가 요청된 알고리즘 중 어느 것도 지원하지 않습니다",
|
||||
"authenticator_timed_out": "인증기가 시간 초과되었습니다",
|
||||
"critical_error_occurred_contact_administrator": "치명적인 오류가 발생했습니다. 관리자에게 연락해주세요.",
|
||||
"sign_in_to": "{name}에 로그인",
|
||||
"client_not_found": "클라이언트를 찾을 수 없습니다",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b>이(가) 다음 정보에 접근하려고 합니다:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "{appName} 계정으로 <b>{client}</b>에 로그인하시겠습니까?",
|
||||
"email": "이메일",
|
||||
"view_your_email_address": "이메일 주소 확인",
|
||||
"profile": "프로필",
|
||||
"view_your_profile_information": "프로필 정보 확인",
|
||||
"groups": "그룹",
|
||||
"view_the_groups_you_are_a_member_of": "멤버인 그룹 정보 확인",
|
||||
"cancel": "취소",
|
||||
"sign_in": "로그인",
|
||||
"try_again": "다시 시도",
|
||||
"client_logo": "클라이언트 로고",
|
||||
"sign_out": "로그아웃",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "{appName}에서 계정 <b>{username}</b>을 로그아웃하시겠습니까?",
|
||||
"sign_in_to_appname": "{appName}에 로그인",
|
||||
"please_try_to_sign_in_again": "다시 로그인해주세요.",
|
||||
"authenticate_with_passkey_to_access_account": "패스키로 본인 인증하여 계정에 접근하세요.",
|
||||
"authenticate": "인증",
|
||||
"please_try_again": "다시 시도해주세요.",
|
||||
"continue": "계속",
|
||||
"alternative_sign_in": "다른 로그인 방법",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "패스키에 접근할 수 없는 경우 다음 방법 중 하나를 이용하여 로그인할 수 있습니다.",
|
||||
"use_your_passkey_instead": "대신 패스키 이용하기",
|
||||
"email_login": "이메일 로그인",
|
||||
"enter_a_login_code_to_sign_in": "로그인 코드를 입력하여 로그인하세요.",
|
||||
"request_a_login_code_via_email": "이메일로 로그인 코드를 요청합니다.",
|
||||
"go_back": "뒤로 가기",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "입력한 이메일 주소가 시스템에 존재하는 경우 이메일이 발송됩니다.",
|
||||
"enter_code": "코드 입력",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "이메일 주소를 입력하여 로그인 코드가 포함된 이메일을 받을 수 있습니다.",
|
||||
"your_email": "이메일 주소",
|
||||
"submit": "확인",
|
||||
"enter_the_code_you_received_to_sign_in": "로그인하기 위해 받은 코드를 입력하세요.",
|
||||
"code": "코드",
|
||||
"invalid_redirect_url": "잘못된 리다이렉트 URL",
|
||||
"audit_log": "감사 로그",
|
||||
"users": "사용자",
|
||||
"user_groups": "사용자 그룹",
|
||||
"oidc_clients": "OIDC 클라이언트",
|
||||
"api_keys": "API 키",
|
||||
"application_configuration": "애플리케이션 설정",
|
||||
"settings": "설정",
|
||||
"update_pocket_id": "Pocket ID 업데이트",
|
||||
"powered_by": "제공:",
|
||||
"see_your_account_activities_from_the_last_3_months": "지난 3개월 동안의 계정 활동을 확인하세요.",
|
||||
"time": "시간",
|
||||
"event": "이벤트",
|
||||
"approximate_location": "대략적인 위치",
|
||||
"ip_address": "IP 주소",
|
||||
"device": "기기",
|
||||
"client": "클라이언트",
|
||||
"unknown": "알 수 없음",
|
||||
"account_details_updated_successfully": "계정 세부 사항이 성공적으로 업데이트되었습니다",
|
||||
"profile_picture_updated_successfully": "프로필 사진이 성공적으로 업데이트되었습니다. 업데이트 적용까지 몇 분 정도 걸릴 수 있습니다.",
|
||||
"account_settings": "계정 설정",
|
||||
"passkey_missing": "패스키가 없습니다",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "계정 접근 권한을 잃지 않기 위해 패스키를 추가해주세요.",
|
||||
"single_passkey_configured": "패스키가 하나만 구성되었습니다",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "계정 접근 권한을 잃지 않기 위해 패스키를 두 개 이상 추가하는 것이 권장됩니다.",
|
||||
"account_details": "계정 세부 사항",
|
||||
"passkeys": "패스키",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "사용자 인증에 사용하는 패스키를 관리하세요.",
|
||||
"add_passkey": "패스키 추가",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "패스키 없이 다른 기기에서 로그인하기 위한 일회용 로그인 코드를 생성합니다.",
|
||||
"create": "생성",
|
||||
"first_name": "이름",
|
||||
"last_name": "성",
|
||||
"username": "사용자 이름",
|
||||
"save": "저장",
|
||||
"username_can_only_contain": "사용자 이름은 영어 소문자, 숫자, 밑줄, 점, 하이픈, '@' 기호만 포함할 수 있습니다",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "다음 코드를 사용하여 로그인하세요. 이 코드는 15분 후에 만료됩니다.",
|
||||
"or_visit": "또는",
|
||||
"added_on": "추가:",
|
||||
"rename": "이름 변경",
|
||||
"delete": "삭제",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "이 패스키를 삭제하시겠습니까?",
|
||||
"passkey_deleted_successfully": "패스키가 성공적으로 삭제되었습니다",
|
||||
"delete_passkey_name": "{passkeyName} 삭제",
|
||||
"passkey_name_updated_successfully": "패스키 이름이 성공적으로 업데이트되었습니다",
|
||||
"name_passkey": "패스키 이름",
|
||||
"name_your_passkey_to_easily_identify_it_later": "패스키의 이름을 지정하여 나중에 쉽게 구분할 수 있도록 합니다.",
|
||||
"create_api_key": "API 키 생성",
|
||||
"add_a_new_api_key_for_programmatic_access": "프로그램 접근을 위해 새로운 API 키를 추가합니다.",
|
||||
"add_api_key": "API 키 추가",
|
||||
"manage_api_keys": "API 키 관리",
|
||||
"api_key_created": "API 키 생성됨",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "보안상의 이유로 이 키는 한 번만 표시됩니다. 안전하게 보관해 주세요.",
|
||||
"description": "설명",
|
||||
"api_key": "API 키",
|
||||
"close": "닫기",
|
||||
"name_to_identify_this_api_key": "API 키를 구분하기 위한 이름.",
|
||||
"expires_at": "만료일",
|
||||
"when_this_api_key_will_expire": "API 키의 만료일.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "이 키의 목적을 알기 위한 설명. (선택)",
|
||||
"expiration_date_must_be_in_the_future": "만료일은 미래의 날짜여야 합니다",
|
||||
"revoke_api_key": "API 키 취소",
|
||||
"never": "없음",
|
||||
"revoke": "취소",
|
||||
"api_key_revoked_successfully": "API 키가 성공적으로 취소되었습니다",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "API 키 \"{apiKeyName}\"를 정말로 취소하시겠습니까? 이 키를 사용하는 모든 통합이 작동하지 않습니다.",
|
||||
"last_used": "마지막 사용",
|
||||
"actions": "동작",
|
||||
"images_updated_successfully": "이미지가 성공적으로 업데이트되었습니다",
|
||||
"general": "일반",
|
||||
"configure_smtp_to_send_emails": "새로운 기기나 위치에서 로그인 감지 시 이메일 알림을 활성화합니다.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP 서버에서 사용자와 그룹을 동기화하기 위해 LDAP을 구성합니다.",
|
||||
"images": "이미지",
|
||||
"update": "업데이트",
|
||||
"email_configuration_updated_successfully": "이메일 설정이 성공적으로 업데이트되었습니다",
|
||||
"save_changes_question": "변경 내용을 저장하시겠습니까?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "테스트 이메일을 전송하기 전에 변경 내용을 저장해야 합니다. 지금 저장하시겠습니까?",
|
||||
"save_and_send": "저장하고 전송하기",
|
||||
"test_email_sent_successfully": "테스트 이메일이 성공적으로 귀하의 이메일 주소에 전송되었습니다.",
|
||||
"failed_to_send_test_email": "테스트 이메일 전송에 실패했씁니다. 자세한 내용을 서버 로그를 확인하세요.",
|
||||
"smtp_configuration": "SMTP 구성",
|
||||
"smtp_host": "SMTP 호스트",
|
||||
"smtp_port": "SMTP 포트",
|
||||
"smtp_user": "SMTP 사용자",
|
||||
"smtp_password": "SMTP 비밀번호",
|
||||
"smtp_from": "SMTP 발신자",
|
||||
"smtp_tls_option": "SMTP TLS 옵션",
|
||||
"email_tls_option": "이메일 TLS 옵션",
|
||||
"skip_certificate_verification": "인증서 검증 건너뛰기",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "이 옵션은 자체 설명 인증서에 유용할 수 있습니다.",
|
||||
"enabled_emails": "이메일 활성화",
|
||||
"email_login_notification": "이메일 로그인 알림",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "사용자가 새로운 기기에서 로그인할 때 이메일을 전송합니다.",
|
||||
"emai_login_code_requested_by_user": "사용자가 요청한 이메일 로그인 코드",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "사용자가 이메일로 전송된 로그인 코드를 요청하여 패스키를 우회할 수 있도록 합니다. 이 기능은 사용자의 이메일 접근 권한이 있는 누구나 접근할 수 있어 보안이 크게 약화됩니다.",
|
||||
"email_login_code_from_admin": "관리자 로그인 코드 전송",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "관리자가 사용자에게 로그인 코드를 전송할 수 있게 합니다.",
|
||||
"send_test_email": "테스트 이메일 보내기",
|
||||
"application_configuration_updated_successfully": "애플리케이션 구성이 성공적으로 업데이트되었습니다",
|
||||
"application_name": "애플리케이션 이름",
|
||||
"session_duration": "세션 기간",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "사용자가 다시 로그인하기 전 세션의 시간(분).",
|
||||
"enable_self_account_editing": "셀프 계정 편집 활성화",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "사용자가 자신의 계정 정보를 편집할 수 있습니다.",
|
||||
"emails_verified": "이메일 인증됨",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "OIDC 클라이언트에게 사용자의 이메일이 인증된 것으로 표시합니다.",
|
||||
"ldap_configuration_updated_successfully": "LDAP 구성이 성공적으로 업데이트되었습니다",
|
||||
"ldap_disabled_successfully": "LDAP가 성공적으로 비활성화되었습니다",
|
||||
"ldap_sync_finished": "LDAP 동기화 완료",
|
||||
"client_configuration": "클라이언트 구성",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP 바인드 DN",
|
||||
"ldap_bind_password": "LDAP 바인드 비밀번호",
|
||||
"ldap_base_dn": "LDAP 베이스 DN",
|
||||
"user_search_filter": "사용자 검색 필터",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "사용자 검색 및 동기화를 위한 검색 필터.",
|
||||
"groups_search_filter": "그룹 검색 필터",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "그룹 검색 및 동기화를 위한 검색 필터.",
|
||||
"attribute_mapping": "속성 매핑",
|
||||
"user_unique_identifier_attribute": "사용자 고유 식별자 속성",
|
||||
"the_value_of_this_attribute_should_never_change": "이 속성의 값은 절대 변경되면 안 됩니다.",
|
||||
"username_attribute": "사용자 이름 속성",
|
||||
"user_mail_attribute": "사용자 이메일 속성",
|
||||
"user_first_name_attribute": "이름 속성",
|
||||
"user_last_name_attribute": "성 속성",
|
||||
"user_profile_picture_attribute": "프로필 사진 속성",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "이 속성의 값은 URL, 바이너리 또는 base64 인코딩된 이미지일 수 있습니다.",
|
||||
"group_members_attribute": "그룹 멤버 속성",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "그룹의 멤버를 질의할 때 사용할 속성.",
|
||||
"group_unique_identifier_attribute": "그룹 고유 식별자 속성",
|
||||
"group_name_attribute": "그룹 멤버 속성",
|
||||
"admin_group_name": "관리자 그룹 이름",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "이 그룹의 멤버들은 Pocket ID에서 관리자 권한을 갖게 됩니다.",
|
||||
"disable": "비활성화",
|
||||
"sync_now": "지금 동기화",
|
||||
"enable": "활성화",
|
||||
"user_created_successfully": "사용자가 성공적으로 생성되었습니다",
|
||||
"create_user": "사용자 생성",
|
||||
"add_a_new_user_to_appname": "{appName}에 새로운 사용자를 추가하세요",
|
||||
"add_user": "사용자 추가",
|
||||
"manage_users": "사용자 관리",
|
||||
"admin_privileges": "관리자 권한",
|
||||
"admins_have_full_access_to_the_admin_panel": "관리자는 관리 패널에 대한 전체 접근 권한을 갖습니다.",
|
||||
"delete_firstname_lastname": "{firstName} {lastName} 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_user": "이 사용자를 삭제하시겠습니까?",
|
||||
"user_deleted_successfully": "사용자가 성공적으로 삭제되었습니다",
|
||||
"role": "역할",
|
||||
"source": "소스",
|
||||
"admin": "관리자",
|
||||
"user": "사용자",
|
||||
"local": "로컬",
|
||||
"toggle_menu": "메뉴 표시 전환",
|
||||
"edit": "편집",
|
||||
"user_groups_updated_successfully": "사용자 그룹이 성공적으로 업데이트되었습니다",
|
||||
"user_updated_successfully": "사용자가 성공적으로 업데이트되었습니다",
|
||||
"custom_claims_updated_successfully": "사용자 정의 클레임이 성공적으로 업데이트되었습니다",
|
||||
"back": "뒤로",
|
||||
"user_details_firstname_lastname": "{firstName} {lastName} 사용자 상세 정보",
|
||||
"manage_which_groups_this_user_belongs_to": "이 사용자가 속한 그룹을 관리합니다.",
|
||||
"custom_claims": "사용자 정의 클레임",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "사용자 정의 클레임은 사용자에 대한 추가 정보를 저장하기 위해 사용되는 키-값 쌍입니다. 이 클레임은 'profile' 스코프가 요청될 경우 ID 토큰에 포함됩니다.",
|
||||
"user_group_created_successfully": "사용자 그룹이 성공적으로 생성되었습니다",
|
||||
"create_user_group": "사용자 그룹 생성",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "사용자에게 할당할 수 있는 새로운 그룹을 생성합니다.",
|
||||
"add_group": "그룹 추가",
|
||||
"manage_user_groups": "사용자 그룹 관리",
|
||||
"friendly_name": "별칭",
|
||||
"name_that_will_be_displayed_in_the_ui": "UI에 표시되는 이름",
|
||||
"name_that_will_be_in_the_groups_claim": "\"groups\" 클레임에 표시되는 이름",
|
||||
"delete_name": "{name} 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "이 사용자 그룹을 삭제하시겠습니까?",
|
||||
"user_group_deleted_successfully": "사용자 그룹이 성공적으로 삭제되었습니다",
|
||||
"user_count": "사용자 수",
|
||||
"user_group_updated_successfully": "사용자 그룹이 성공적으로 업데이트되었습니다",
|
||||
"users_updated_successfully": "사용자가 성공적으로 업데이트되었습니다",
|
||||
"user_group_details_name": "{name} 사용자 그룹 상세 정보",
|
||||
"assign_users_to_this_group": "이 그룹에 사용자를 할당합니다.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "사용자 정의 클레임은 사용자에 대한 추가 정보를 저장하기 위해 사용되는 키-값 쌍입니다. 이 클레임은 'profile' 스코프가 요청될 경우 ID 토큰에 포함됩니다. 사용자 정의 클레임은 충돌이 있을 경우 우선 순위를 갖습니다.",
|
||||
"oidc_client_created_successfully": "OIDC 클라이언트가 성공적으로 생성되었습니다",
|
||||
"create_oidc_client": "OIDC 클라이언트 생성",
|
||||
"add_a_new_oidc_client_to_appname": "{appName}에 새로운 OIDC 클라이언트를 추가합니다.",
|
||||
"add_oidc_client": "OIDC 클라이언트 추가",
|
||||
"manage_oidc_clients": "OIDC 클라이언트 관리",
|
||||
"one_time_link": "일회용 링크",
|
||||
"use_this_link_to_sign_in_once": "이 링크를 사용하여 한 번 로그인하세요. 이 기능은 패스키를 추가하지 않았거나 패스키를 분실한 사용자에게 필요합니다.",
|
||||
"add": "추가",
|
||||
"callback_urls": "콜백 URL",
|
||||
"logout_callback_urls": "로그아웃 콜백 URL",
|
||||
"public_client": "공개 클라이언트",
|
||||
"public_clients_description": "공개 클라이언트는 클라이언트 시크릿이 없습니다. 이들은 시크릿을 안전하게 보관할 수 없는 모바일, 웹, 네이티브 애플리케이션을 위해 설계되었습니다.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "공개 키 코드 교환은 CSRF 및 승인 코드 가로채기 공격을 방지하기 위한 보안 기능입니다.",
|
||||
"name_logo": "{name} 로고",
|
||||
"change_logo": "로고 변경",
|
||||
"upload_logo": "로고 업로드",
|
||||
"remove_logo": "로고 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "이 OIDC 클라이언트를 삭제하시겠습니까?",
|
||||
"oidc_client_deleted_successfully": "OIDC 클라이언트가 성공적으로 삭제되었습니다",
|
||||
"authorization_url": "승인 URL",
|
||||
"oidc_discovery_url": "OIDC 디스커버리 URL",
|
||||
"token_url": "토큰 URL",
|
||||
"userinfo_url": "사용자 정보 URL",
|
||||
"logout_url": "로그아웃 URL",
|
||||
"certificate_url": "인증서 URL",
|
||||
"enabled": "활성화됨",
|
||||
"disabled": "비활성화됨",
|
||||
"oidc_client_updated_successfully": "OIDC 클라이언트가 성공적으로 업데이트되었습니다",
|
||||
"create_new_client_secret": "새로운 클라이언트 시크릿 생성",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "새로운 클라이언트 시크릿 생성하시겠습니까? 기존 클라이언트 시크릿은 무효화됩니다.",
|
||||
"generate": "생성",
|
||||
"new_client_secret_created_successfully": "새로운 클라이언트 시크릿이 성공적으로 생성되었습니다",
|
||||
"allowed_user_groups_updated_successfully": "허용된 사용자 그룹이 성공적으로 업데이트되었습니다",
|
||||
"oidc_client_name": "OIDC 클라이언트 {name}",
|
||||
"client_id": "클라이언트 ID",
|
||||
"client_secret": "클라이언트 시크릿",
|
||||
"show_more_details": "상세 정보 보기",
|
||||
"allowed_user_groups": "허용된 사용자 그룹",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "이 클라이언트에 사용자 그룹을 추가하여 해당 그룹의 사용자의 접근를 제한합니다. 사용자 그룹을 선택하지 않으면 모든 사용자가 이 클라이언트에 접근할 수 있습니다.",
|
||||
"favicon": "파비콘",
|
||||
"light_mode_logo": "라이트 모드 로고",
|
||||
"dark_mode_logo": "다크 모드 로고",
|
||||
"background_image": "배경 이미지",
|
||||
"language": "언어",
|
||||
"reset_profile_picture_question": "프로필 사진을 재설정하시겠습니까?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "업로드한 이미지를 삭제하고 프로필 이미지를 기본값으로 되돌립니다. 계속하시겠습니까?",
|
||||
"reset": "재설정",
|
||||
"reset_to_default": "기본값으로 재설정",
|
||||
"profile_picture_has_been_reset": "프로필 사진이 재설정되었습니다. 업데이트에 몇 분 정도 소요될 수 있습니다.",
|
||||
"select_the_language_you_want_to_use": "사용할 언어를 선택하세요. 일부 텍스트는 자동으로 번역되었을 수 있으며, 정확하지 않을 수 있습니다.",
|
||||
"contribute_to_translation": "문제를 발견했다면 <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>에서 번역에 기여해 주세요.",
|
||||
"personal": "개인",
|
||||
"global": "전역",
|
||||
"all_users": "모든 사용자",
|
||||
"all_events": "모든 이벤트",
|
||||
"all_clients": "모든 클라이언트",
|
||||
"all_locations": "모든 위치",
|
||||
"global_audit_log": "전역 감사 로그",
|
||||
"see_all_account_activities_from_the_last_3_months": "지난 3개월 동안의 모든 사용자 활동을 확인하세요.",
|
||||
"token_sign_in": "토큰 로그인",
|
||||
"client_authorization": "클라이언트 승인",
|
||||
"new_client_authorization": "새로운 클라이언트 승인",
|
||||
"disable_animations": "애니메이션 비활성화",
|
||||
"turn_off_ui_animations": "UI 전체의 애니메이션을 비활성화합니다.",
|
||||
"user_disabled": "계정 비활성화",
|
||||
"disabled_users_cannot_log_in_or_use_services": "비활성화된 사용자는 로그인하거나 서비스를 사용할 수 없습니다.",
|
||||
"user_disabled_successfully": "사용자가 성공적으로 비활성화되었습니다.",
|
||||
"user_enabled_successfully": "사용자가 성공적으로 활성화되었습니다.",
|
||||
"status": "상태",
|
||||
"disable_firstname_lastname": "{firstName} {lastName} 비활성화",
|
||||
"are_you_sure_you_want_to_disable_this_user": "이 사용자를 비활성화하시겠습니까? 이 사용자는 로그인하거나 어떤 서비스에도 접근할 수 없게 됩니다.",
|
||||
"ldap_soft_delete_users": "LDAP에 비활성화된 사용자 유지",
|
||||
"ldap_soft_delete_users_description": "이 기능이 활성화되면 LDAP에서 삭제된 사용자는 시스템에서 삭제되지 않고 비활성화됩니다.",
|
||||
"login_code_email_success": "로그인 코드가 사용자에게 전송되었습니다.",
|
||||
"send_email": "이메일 전송",
|
||||
"show_code": "코드 표시",
|
||||
"callback_url_description": "클라이언트가 제공한 URL. 비워둔 경우 자동으로 추가됩니다. 와일드카드(*)도 지원하지만, 보안상의 이유로 사용을 권장하지 않습니다.",
|
||||
"logout_callback_url_description": "클라이언트가 제공한 로그아웃 URL. 와일드카드(*)도 지원하지만, 보안상의 이유로 사용을 권장하지 않습니다.",
|
||||
"api_key_expiration": "API 키 만료",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API 키가 만료되기 전에 사용자에게 이메일을 전송합니다.",
|
||||
"authorize_device": "기기 승인",
|
||||
"the_device_has_been_authorized": "기기가 승인되었습니다.",
|
||||
"enter_code_displayed_in_previous_step": "이전 단계에 표시된 코드를 입력하세요.",
|
||||
"authorize": "승인",
|
||||
"federated_client_credentials": "연동 클라이언트 자격 증명",
|
||||
"federated_client_credentials_description": "연동 클라이언트 자격 증명을 이용하여, OIDC 클라이언트를 제3자 인증 기관에서 발급한 JWT 토큰을 이용해 인증할 수 있습니다.",
|
||||
"add_federated_client_credential": "연동 클라이언트 자격 증명 추가",
|
||||
"add_another_federated_client_credential": "다른 연동 클라이언트 자격 증명 추가",
|
||||
"oidc_allowed_group_count": "허용된 그룹 수",
|
||||
"unrestricted": "제한 없음",
|
||||
"show_advanced_options": "고급 옵션 표시",
|
||||
"hide_advanced_options": "고급 옵션 숨기기",
|
||||
"oidc_data_preview": "OIDC 데이터 미리보기",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "여러 사용자를 위해 전송될 OIDC 데이터 미리보기",
|
||||
"id_token": "ID 토큰",
|
||||
"access_token": "접근 토큰",
|
||||
"userinfo": "사용자 정보",
|
||||
"id_token_payload": "ID 토큰 페이로드",
|
||||
"access_token_payload": "접근 토큰 페이로드",
|
||||
"userinfo_endpoint_response": "사용자 정보 엔드포인트 응답",
|
||||
"copy": "복사",
|
||||
"no_preview_data_available": "미리보기 데이터가 없습니다",
|
||||
"copy_all": "모두 복사",
|
||||
"preview": "미리보기",
|
||||
"preview_for_user": "{name} ({email}) 미리보기",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "이 사용자를 위해 전송될 OIDC 데이터 미리보기",
|
||||
"show": "표시",
|
||||
"select_an_option": "옵션 선택",
|
||||
"select_user": "사용자 선택",
|
||||
"error": "오류",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Pocket ID의 외관을 맞춤 설정하려면 강조 색상을 선택하세요.",
|
||||
"accent_color": "강조 색상",
|
||||
"custom_accent_color": "맞춤 강조 색상",
|
||||
"custom_accent_color_description": "유효한 CSS 색상 형식(예: hex, rgb, hsl)을 사용하여 맞춤 색상을 입력하세요.",
|
||||
"color_value": "색상 값",
|
||||
"apply": "적용",
|
||||
"signup_token": "계정 생성 토큰",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "새로운 사용자 등록을 허용하기 위해 계정 생성 토큰을 생성합니다.",
|
||||
"usage_limit": "사용량 제한",
|
||||
"number_of_times_token_can_be_used": "계정 생성 토큰을 사용할 수 있는 횟수.",
|
||||
"expires": "만료일",
|
||||
"signup": "계정 생성",
|
||||
"signup_requires_valid_token": "계정을 생성하려면 유효한 계정 생성 토큰이 필요합니다",
|
||||
"validating_signup_token": "계정 생성 토큰 검증",
|
||||
"go_to_login": "로그인으로 이동",
|
||||
"signup_to_appname": "{appName} 계정 생성하기",
|
||||
"create_your_account_to_get_started": "계정을 만들어 시작하세요.",
|
||||
"initial_account_creation_description": "시작하기 위해 계정을 만드세요. 패스키를 나중에 설정할 수 있습니다.",
|
||||
"setup_your_passkey": "패스키 설정",
|
||||
"create_a_passkey_to_securely_access_your_account": "계정에 안전하게 접근하기 위해 패스키를 생성하세요. 이 패스키는 로그인을 위한 주요 방법으로 사용됩니다.",
|
||||
"skip_for_now": "지금은 건너뛰기",
|
||||
"account_created": "계정 생성됨",
|
||||
"enable_user_signups": "계정 생성 활성화",
|
||||
"enable_user_signups_description": "계정 생성 기능이 활성화됩니다.",
|
||||
"user_signups_are_disabled": "계정 생성이 현재 비활성화되었습니다",
|
||||
"create_signup_token": "계정 생성 토큰 생성",
|
||||
"view_active_signup_tokens": "활성 계정 생성 토큰 보기",
|
||||
"manage_signup_tokens": "계정 생성 토큰 관리",
|
||||
"view_and_manage_active_signup_tokens": "활성 계정 생성 토큰을 조회하고 관리합니다.",
|
||||
"signup_token_deleted_successfully": "계정 생성 토큰이 성공적으로 삭제되었습니다.",
|
||||
"expired": "만료됨",
|
||||
"used_up": "사용 완료",
|
||||
"active": "활성",
|
||||
"usage": "사용량",
|
||||
"created": "생성일",
|
||||
"token": "토큰",
|
||||
"loading": "불러오는 중",
|
||||
"delete_signup_token": "계정 생성 토큰 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "이 계정 생성 토큰을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||
"signup_disabled_description": "계정 생성이 완전히 비활성화됩니다. 새로운 사용자 계정은 관리자만 생성할 수 있습니다.",
|
||||
"signup_with_token": "토큰으로 계정 생성",
|
||||
"signup_with_token_description": "사용자는 관리자가 생성한 유효한 계정 생성 토큰을 사용해야 가입할 수 있습니다.",
|
||||
"signup_open": "계정 생성 허용",
|
||||
"signup_open_description": "누구나 제한 없이 새로운 계정을 생성할 수 있습니다.",
|
||||
"of": "의",
|
||||
"skip_passkey_setup": "패스키 설정 건너뛰기",
|
||||
"skip_passkey_setup_description": "패스키를 설정하는 것이 강력히 권장됩니다. 패스키가 없으면 세션이 만료되자마자 계정에 접근할 수 없게 됩니다.",
|
||||
"my_apps": "내 앱",
|
||||
"no_apps_available": "사용 가능한 앱이 없습니다",
|
||||
"contact_your_administrator_for_app_access": "관리자에게 연락하여 앱의 접근 권한을 얻으세요.",
|
||||
"launch": "실행",
|
||||
"client_launch_url": "클라이언트 실행 URL",
|
||||
"client_launch_url_description": "사용자가 '내 앱' 페이지에서 앱을 실행할 때 열릴 URL입니다.",
|
||||
"client_name_description": "Pocket ID UI에 표시되는 클라이언트의 이름입니다.",
|
||||
"revoke_access": "접근 권한 취소",
|
||||
"revoke_access_description": "<b>{clientName}</b>의 접근 권한을 취소합니다. <b>{clientName}</b>은 더 이상 계정 정보에 접근할 수 없습니다.",
|
||||
"revoke_access_successful": "{clientName}의 접근이 성공적으로 취소되었습니다.",
|
||||
"last_signed_in_ago": "{time} 전에 로그인함"
|
||||
}
|
||||
@@ -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",
|
||||
"dont_have_access_to_your_passkey": "Heb je geen toegang tot je passkey?",
|
||||
"login_background": "Inlogachtergrond",
|
||||
"logo": "Logo",
|
||||
"login_code": "Inlogcode",
|
||||
@@ -42,46 +42,46 @@
|
||||
"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",
|
||||
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen vaste sleutels",
|
||||
"passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen",
|
||||
"authenticator_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 passkeys, kun je je op een van de volgende manieren aanmelden.",
|
||||
"use_your_passkey_instead": "Wil je in plaats daarvan je toegangscode 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",
|
||||
@@ -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",
|
||||
"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.",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat je de toegang tot je account verliest.",
|
||||
"single_passkey_configured": "Eén enkele toegangscode geconfigureerd",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat u de toegang tot uw account verliest.",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat je de toegang tot uw account verliest.",
|
||||
"account_details": "Accountgegevens",
|
||||
"passkeys": "Toegangscodes",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee je jezelf kunt verifiëren.",
|
||||
"add_passkey": "Passkey toevoegen",
|
||||
"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,19 @@
|
||||
"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.",
|
||||
"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 +292,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,12 +308,12 @@
|
||||
"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",
|
||||
@@ -325,33 +325,33 @@
|
||||
"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 +378,57 @@
|
||||
"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.",
|
||||
"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 toegangscode aan om veilig toegang te krijgen tot je account. Dit is je primaire manier om in te loggen.",
|
||||
"skip_for_now": "Voor nu even overslaan",
|
||||
"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.",
|
||||
"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.",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Weet u zeker dat u dit aanmeldingstoken wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
|
||||
"signup_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 echt geblokkeerd.",
|
||||
"last_signed_in_ago": "Laatst ingelogd {time} geleden"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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": "Фамилия",
|
||||
@@ -419,15 +419,16 @@
|
||||
"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} назад"
|
||||
}
|
||||
|
||||
@@ -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": "Групу користувачів успішно видалено",
|
||||
@@ -420,14 +420,15 @@
|
||||
"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} тому"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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}"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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} 前"
|
||||
}
|
||||
|
||||
@@ -429,5 +429,6 @@
|
||||
"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} 前"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"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",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"ko",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
140
frontend/src/lib/components/form/searchable-multi-select.svelte
Normal file
140
frontend/src/lib/components/form/searchable-multi-select.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
isLoading = true;
|
||||
try {
|
||||
const user = await userService.exchangeOneTimeAccessToken(code);
|
||||
userStore.setUser(user);
|
||||
await userStore.setUser(user);
|
||||
|
||||
try {
|
||||
goto(data.redirect);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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())
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
@@ -70,7 +70,7 @@
|
||||
{#if expandAddClient}
|
||||
<div transition:slide>
|
||||
<Card.Content>
|
||||
<OIDCClientForm callback={createOIDCClient} />
|
||||
<OIDCClientForm mode="create" callback={createOIDCClient} />
|
||||
</Card.Content>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
userStore.setUser(result.data);
|
||||
await userStore.setUser(result.data);
|
||||
isLoading = false;
|
||||
|
||||
goto('/signup/add-passkey');
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
userStore.setUser(result.data);
|
||||
await userStore.setUser(result.data);
|
||||
isLoading = false;
|
||||
|
||||
goto('/signup/add-passkey');
|
||||
|
||||
BIN
frontend/static/fonts/GoogleSansCode-SemiBold.ttf
Normal file
BIN
frontend/static/fonts/GoogleSansCode-SemiBold.ttf
Normal file
Binary file not shown.
179
pnpm-lock.yaml
generated
179
pnpm-lock.yaml
generated
@@ -22,6 +22,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
jose:
|
||||
specifier: ^5.10.0
|
||||
version: 5.10.0
|
||||
@@ -101,6 +104,9 @@ importers:
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.6.14
|
||||
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.36.17))(prettier@3.6.2)
|
||||
rollup:
|
||||
specifier: ^4.46.3
|
||||
version: 4.46.3
|
||||
svelte:
|
||||
specifier: ^5.36.16
|
||||
version: 5.36.17
|
||||
@@ -474,103 +480,103 @@ packages:
|
||||
'@poppinss/macroable@1.0.5':
|
||||
resolution: {integrity: sha512-6u61y1HHd090MEk1Av0/1btDmm2Hh/+XoJj+HgFYRh9koUPI822ybJbwLHuqjLNCiY+o1gRykg2igEqOf/VBZw==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.45.3':
|
||||
resolution: {integrity: sha512-8oQkCTve4H4B4JpmD2FV7fV2ZPTxJHN//bRhCqPUU8v6c5APlxteAXyc7BFaEb4aGpUzrPLU4PoAcGhwmRzZTA==}
|
||||
'@rollup/rollup-android-arm-eabi@4.46.3':
|
||||
resolution: {integrity: sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.45.3':
|
||||
resolution: {integrity: sha512-StOsmdXHU2hx3UFTTs6yYxCSwSIgLsfjUBICXyWj625M32OOjakXlaZuGKL+jA3Nvv35+hMxrm/64eCoT07SYQ==}
|
||||
'@rollup/rollup-android-arm64@4.46.3':
|
||||
resolution: {integrity: sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.45.3':
|
||||
resolution: {integrity: sha512-6CfLF3eqKhCdhK0GUnR5ZS99OFz+dtOeB/uePznLKxjCsk5QjT/V0eSEBb4vj+o/ri3i35MseSEQHCLLAgClVw==}
|
||||
'@rollup/rollup-darwin-arm64@4.46.3':
|
||||
resolution: {integrity: sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.45.3':
|
||||
resolution: {integrity: sha512-QLWyWmAJG9elNTNLdcSXUT/M+J7DhEmvs1XPHYcgYkse3UHf9iWTJ+yTPlKMIetiQnNi+cNp+gY4gvjDpREfKw==}
|
||||
'@rollup/rollup-darwin-x64@4.46.3':
|
||||
resolution: {integrity: sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.45.3':
|
||||
resolution: {integrity: sha512-ZOvBq+5nL0yrZIEo1eq6r7MPvkJ8kC1XATS/yHvcq3WbDNKNKBQ1uIF4hicyzDMoJt72G+sn1nKsFXpifZyRDA==}
|
||||
'@rollup/rollup-freebsd-arm64@4.46.3':
|
||||
resolution: {integrity: sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.45.3':
|
||||
resolution: {integrity: sha512-AYvGR07wecEnyYSovyJ71pTOulbNvsrpRpK6i/IM1b0UGX1vFx51afYuPYPxnvE9aCl5xPnhQicEvdIMxClRgQ==}
|
||||
'@rollup/rollup-freebsd-x64@4.46.3':
|
||||
resolution: {integrity: sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.45.3':
|
||||
resolution: {integrity: sha512-Yx8Cp38tfRRToVLuIWzBHV25/QPzpUreOPIiUuNV7KahNPurYg2pYQ4l7aYnvpvklO1riX4643bXLvDsYSBIrA==}
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.46.3':
|
||||
resolution: {integrity: sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.45.3':
|
||||
resolution: {integrity: sha512-4dIYRNxlXGDKnO6qgcda6LxnObPO6r1OBU9HG8F9pAnHHLtfbiOqCzDvkeHknx+5mfFVH4tWOl+h+cHylwsPWA==}
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.46.3':
|
||||
resolution: {integrity: sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.45.3':
|
||||
resolution: {integrity: sha512-M6uVlWKmhLN7LguLDu6396K1W5IBlAaRonjlHQgc3s4dOGceu0FeBuvbXiUPYvup/6b5Ln7IEX7XNm68DN4vrg==}
|
||||
'@rollup/rollup-linux-arm64-gnu@4.46.3':
|
||||
resolution: {integrity: sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.45.3':
|
||||
resolution: {integrity: sha512-emaYiOTQJUd6fC9a6jcw9zIWtzaUiuBC+vomggaM4In2iOra/lA6IMHlqZqQZr08NYXrOPMVigreLMeSAwv3Uw==}
|
||||
'@rollup/rollup-linux-arm64-musl@4.46.3':
|
||||
resolution: {integrity: sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.45.3':
|
||||
resolution: {integrity: sha512-3P77T5AQ4UfVRJSrTKLiUZDJ6XsxeP80027bp6mOFh8sevSD038mYuIYFiUtrSJxxgFb+NgRJFF9oIa0rlUsmg==}
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.46.3':
|
||||
resolution: {integrity: sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.45.3':
|
||||
resolution: {integrity: sha512-/VPH3ZVeSlmCBPhZdx/+4dMXDjaGMhDsWOBo9EwSkGbw2+OAqaslL53Ao2OqCxR0GgYjmmssJ+OoG+qYGE7IBg==}
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.46.3':
|
||||
resolution: {integrity: sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.45.3':
|
||||
resolution: {integrity: sha512-Hs5if0PjROl1MGMmZX3xMAIfqcGxQE2SJWUr/CpDQsOQn43Wq4IvXXxUMWtiY/BrzdqCCJlRgJ5DKxzS3qWkCw==}
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.46.3':
|
||||
resolution: {integrity: sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.45.3':
|
||||
resolution: {integrity: sha512-Qm0WOwh3Lk388+HJFl1ILGbd2iOoQf6yl4fdGqOjBzEA+5JYbLcwd+sGsZjs5pkt8Cr/1G42EiXmlRp9ZeTvFA==}
|
||||
'@rollup/rollup-linux-riscv64-musl@4.46.3':
|
||||
resolution: {integrity: sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.45.3':
|
||||
resolution: {integrity: sha512-VJdknTaYw+TqXzlh9c7vaVMh/fV2sU8Khfk4a9vAdYXJawpjf6z3U1k7vDWx2IQ9ZOPoOPxgVpDfYOYhxD7QUA==}
|
||||
'@rollup/rollup-linux-s390x-gnu@4.46.3':
|
||||
resolution: {integrity: sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.45.3':
|
||||
resolution: {integrity: sha512-SUDXU5YabLAMl86FpupSQQEWzVG8X0HM+Q/famnJusbPiUgQnTGuSxtxg4UAYgv1ZmRV1nioYYXsgtSokU/7+Q==}
|
||||
'@rollup/rollup-linux-x64-gnu@4.46.3':
|
||||
resolution: {integrity: sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.45.3':
|
||||
resolution: {integrity: sha512-ezmqknOUFgZMN6wW+Avlo4sXF3Frswd+ncrwMz4duyZ5Eqd+dAYgJ+A1MY+12LNZ7XDhCiijJceueYvtnzdviw==}
|
||||
'@rollup/rollup-linux-x64-musl@4.46.3':
|
||||
resolution: {integrity: sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.45.3':
|
||||
resolution: {integrity: sha512-1YfXoUEE++gIW66zNB9Twd0Ua5xCXpfYppFUxVT/Io5ZT3fO6Se+C/Jvmh3usaIHHyi53t3kpfjydO2GAy5eBA==}
|
||||
'@rollup/rollup-win32-arm64-msvc@4.46.3':
|
||||
resolution: {integrity: sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.45.3':
|
||||
resolution: {integrity: sha512-Iok2YA3PvC163rVZf2Zy81A0g88IUcSPeU5pOilcbICXre2EP1mxn1Db/l09Z/SK1vdSLtpJXAnwGuMOyf5O9g==}
|
||||
'@rollup/rollup-win32-ia32-msvc@4.46.3':
|
||||
resolution: {integrity: sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.45.3':
|
||||
resolution: {integrity: sha512-HwHCH5GQTOeGYP5wBEBXFVhfQecwRl24Rugoqhh8YwGarsU09bHhOKuqlyW4ZolZCan3eTUax7UJbGSmKSM51A==}
|
||||
'@rollup/rollup-win32-x64-msvc@4.46.3':
|
||||
resolution: {integrity: sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -977,6 +983,9 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
@@ -1763,8 +1772,8 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rollup@4.45.3:
|
||||
resolution: {integrity: sha512-STwyHZF3G+CrmZhB+qDiROq9s8B5PrOCYN6dtmOvwz585XBnyeHk1GTEhHJtUVb355/9uZhOazyVclTt5uahzA==}
|
||||
rollup@4.46.3:
|
||||
resolution: {integrity: sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -2423,64 +2432,64 @@ snapshots:
|
||||
'@poppinss/macroable@1.0.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.45.3':
|
||||
'@rollup/rollup-android-arm-eabi@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.45.3':
|
||||
'@rollup/rollup-android-arm64@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.45.3':
|
||||
'@rollup/rollup-darwin-arm64@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.45.3':
|
||||
'@rollup/rollup-darwin-x64@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.45.3':
|
||||
'@rollup/rollup-freebsd-arm64@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.45.3':
|
||||
'@rollup/rollup-freebsd-x64@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.45.3':
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.45.3':
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.45.3':
|
||||
'@rollup/rollup-linux-arm64-gnu@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.45.3':
|
||||
'@rollup/rollup-linux-arm64-musl@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.45.3':
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.45.3':
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.45.3':
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.45.3':
|
||||
'@rollup/rollup-linux-riscv64-musl@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.45.3':
|
||||
'@rollup/rollup-linux-s390x-gnu@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.45.3':
|
||||
'@rollup/rollup-linux-x64-gnu@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.45.3':
|
||||
'@rollup/rollup-linux-x64-musl@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.45.3':
|
||||
'@rollup/rollup-win32-arm64-msvc@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.45.3':
|
||||
'@rollup/rollup-win32-ia32-msvc@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.45.3':
|
||||
'@rollup/rollup-win32-x64-msvc@4.46.3':
|
||||
optional: true
|
||||
|
||||
'@sideway/address@4.1.5':
|
||||
@@ -2919,6 +2928,8 @@ snapshots:
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.13:
|
||||
optional: true
|
||||
|
||||
@@ -3569,30 +3580,30 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rollup@4.45.3:
|
||||
rollup@4.46.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.45.3
|
||||
'@rollup/rollup-android-arm64': 4.45.3
|
||||
'@rollup/rollup-darwin-arm64': 4.45.3
|
||||
'@rollup/rollup-darwin-x64': 4.45.3
|
||||
'@rollup/rollup-freebsd-arm64': 4.45.3
|
||||
'@rollup/rollup-freebsd-x64': 4.45.3
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.45.3
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.45.3
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.45.3
|
||||
'@rollup/rollup-linux-arm64-musl': 4.45.3
|
||||
'@rollup/rollup-linux-loongarch64-gnu': 4.45.3
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.45.3
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.45.3
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.45.3
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.45.3
|
||||
'@rollup/rollup-linux-x64-gnu': 4.45.3
|
||||
'@rollup/rollup-linux-x64-musl': 4.45.3
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.45.3
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.45.3
|
||||
'@rollup/rollup-win32-x64-msvc': 4.45.3
|
||||
'@rollup/rollup-android-arm-eabi': 4.46.3
|
||||
'@rollup/rollup-android-arm64': 4.46.3
|
||||
'@rollup/rollup-darwin-arm64': 4.46.3
|
||||
'@rollup/rollup-darwin-x64': 4.46.3
|
||||
'@rollup/rollup-freebsd-arm64': 4.46.3
|
||||
'@rollup/rollup-freebsd-x64': 4.46.3
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.46.3
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.46.3
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.46.3
|
||||
'@rollup/rollup-linux-arm64-musl': 4.46.3
|
||||
'@rollup/rollup-linux-loongarch64-gnu': 4.46.3
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.46.3
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.46.3
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.46.3
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.46.3
|
||||
'@rollup/rollup-linux-x64-gnu': 4.46.3
|
||||
'@rollup/rollup-linux-x64-musl': 4.46.3
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.46.3
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.46.3
|
||||
'@rollup/rollup-win32-x64-msvc': 4.46.3
|
||||
fsevents: 2.3.3
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -3887,7 +3898,7 @@ snapshots:
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.45.3
|
||||
rollup: 4.46.3
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 22.16.5
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user