Compare commits

..

11 Commits

Author SHA1 Message Date
ItalyPaleAle
b11860b864 Add initial REST endpoints 2025-12-07 15:49:45 -08:00
Alessandro (Ale) Segala
c4bd20a90d Merge branch 'main' into breaking/v2 2025-12-07 12:38:12 -08:00
Elias Schneider
900f8fe240 post merge fix 2025-11-30 18:48:10 +01:00
Elias Schneider
1da694f8ad Merge branch 'main' into v2-main 2025-11-30 18:28:00 +01:00
Kyle Mendell
e06538a101 feat: remove DbProvider env variable and calculate it dynamically (#1114) 2025-11-27 12:38:06 -08:00
Elias Schneider
39c1f93756 feat: add CLI command for importing and exporting Pocket ID data (#998)
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 10:38:15 +01:00
ItalyPaleAle
e306d6eb58 Merge remote-tracking branch 'origin' into breaking/v2 2025-11-25 20:48:03 -08:00
Elias Schneider
46793fe68a Merge branch 'main' into v2 2025-11-17 08:39:36 +01:00
Elias Schneider
e22822890f feat!: drop support for storing JWK on the filesystem (#1088) 2025-11-14 12:02:51 +01:00
Elias Schneider
2694d79add Merge branch 'main' into v2 2025-11-14 11:08:20 +01:00
Elias Schneider
98cf1f66c3 fix!: rename LDAP_ATTRIBUTE_ADMIN_GROUP env variable to LDAP_ADMIN_GROUP_NAME (#1089) 2025-11-12 11:51:01 +01:00
213 changed files with 2672 additions and 7642 deletions

View File

@@ -2,7 +2,7 @@ name: Run Backend Linter
on:
push:
branches: [main]
branches: [main, breaking/**]
paths:
- "backend/**"
pull_request:

View File

@@ -1,7 +1,7 @@
name: E2E Tests
on:
push:
branches: [main]
branches: [main, breaking/**]
paths-ignore:
- "docs/**"
- "**.md"
@@ -117,21 +117,6 @@ jobs:
if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar
- name: Cache SCIM Test Server Docker image
uses: actions/cache@v4
id: scim-cache
with:
path: /tmp/scim-test-server-image.tar
key: scim-test-server-${{ runner.os }}
- name: Pull and save SCIM Test Server image
if: steps.scim-cache.outputs.cache-hit != 'true'
run: |
docker pull ghcr.io/pocket-id/scim-test-server
docker save ghcr.io/pocket-id/scim-test-server > /tmp/scim-test-server-image.tar
- name: Load SCIM Test Server image
if: steps.scim-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/scim-test-server-image.tar
- name: Cache Localstack S3 Docker image
if: matrix.storage == 's3'
uses: actions/cache@v4
@@ -186,12 +171,7 @@ jobs:
run: |
DOCKER_COMPOSE_FILE=docker-compose.yml
cat > .env <<EOF
FILE_BACKEND=${{ matrix.storage }}
SCIM_SERVICE_PROVIDER_URL=http://localhost:18123/v2
SCIM_SERVICE_PROVIDER_URL_INTERNAL=http://scim-test-server:8080/v2
EOF
echo "FILE_BACKEND=${{ matrix.storage }}" > .env
if [ "${{ matrix.db }}" = "postgres" ]; then
DOCKER_COMPOSE_FILE=docker-compose-postgres.yml
elif [ "${{ matrix.storage }}" = "s3" ]; then

View File

@@ -2,7 +2,7 @@ name: Svelte Check
on:
push:
branches: [main]
branches: [main, breaking/**]
paths:
- "frontend/src/**"
- ".github/svelte-check-matcher.json"

View File

@@ -1,7 +1,7 @@
name: Unit Tests
on:
push:
branches: [main]
branches: [main, breaking/**]
paths:
- "backend/**"
pull_request:

View File

@@ -1 +1 @@
2.0.0
1.16.0

View File

@@ -1,47 +1,3 @@
## v2.0.0
### Bug Fixes
- update image format message to include WEBP ([#1133](https://github.com/pocket-id/pocket-id/pull/1133) by @sebdanielsson)
- add Japanese locale to inlang settings ([#1142](https://github.com/pocket-id/pocket-id/pull/1142) by @tai-ga)
- restrict email one time sign in token to same browser ([#1144](https://github.com/pocket-id/pocket-id/pull/1144) by @stonith404)
- rename `LDAP_ATTRIBUTE_ADMIN_GROUP` env variable to `LDAP_ADMIN_GROUP_NAME` ([e1c5021](https://github.com/pocket-id/pocket-id/commit/e1c5021eeedcbc54bad0eccd72d7ae760be61934) by @stonith404)
- make wildcard matching in callback URLs more stricter ([078152d](https://github.com/pocket-id/pocket-id/commit/078152d4dbb05dd027ff323f39d090ecb67927c7) by @stonith404)
- remove ambiguous characters from login code ([d9e7bf9](https://github.com/pocket-id/pocket-id/commit/d9e7bf9eef522d8c081fac2000bace6f95518039) by @stonith404)
- add missing translations to date picker ([894eaf3](https://github.com/pocket-id/pocket-id/commit/894eaf3cffdd9182b9c29e28b4dcb7e8bcbda26b) by @stonith404)
### Features
- add HTTP `HEAD` method support ([#1135](https://github.com/pocket-id/pocket-id/pull/1135) by @stonith404)
- add email logo customization ([#1150](https://github.com/pocket-id/pocket-id/pull/1150) by @MelvinSnijders)
- add ability define user groups for sign up tokens ([#1155](https://github.com/pocket-id/pocket-id/pull/1155) by @stonith404)
- minor redesign of auth pages ([08e4ffe](https://github.com/pocket-id/pocket-id/commit/08e4ffeb600a4a6644d91b1600b0205997ed1685) by @stonith404)
- allow audit log retention to be controlled by env variable ([#1158](https://github.com/pocket-id/pocket-id/pull/1158) by @jenic)
- restrict oidc clients by user groups per default ([#1164](https://github.com/pocket-id/pocket-id/pull/1164) by @stonith404)
- add "restricted" column to oidc client table ([1bc9f5f](https://github.com/pocket-id/pocket-id/commit/1bc9f5f7e780310d81608381544ba530df7f433b) by @stonith404)
- drop support for storing JWK on the filesystem ([f014458](https://github.com/pocket-id/pocket-id/commit/f0144584af90b918a3157a298f1bb95928a117b8) by @stonith404)
- add CLI command for importing and exporting Pocket ID data ([3420a00](https://github.com/pocket-id/pocket-id/commit/3420a000737d89a5c3c6c250d171d96126553beb) by @stonith404)
- remove DbProvider env variable and calculate it dynamically ([ba2f0f1](https://github.com/pocket-id/pocket-id/commit/ba2f0f18f4bacc5a86217dec0b0dcb6030c40cb9) by @kmendell)
- add support for SCIM provisioning ([#1182](https://github.com/pocket-id/pocket-id/pull/1182) by @stonith404)
### Other
- update AAGUIDs ([#1128](https://github.com/pocket-id/pocket-id/pull/1128) by @github-actions[bot])
- fix api key e2e test ([25f67bd](https://github.com/pocket-id/pocket-id/commit/25f67bd25a0ee0cab48d72107722e8c8428fa547) by @stonith404)
- update AAGUIDs ([#1140](https://github.com/pocket-id/pocket-id/pull/1140) by @github-actions[bot])
- upgrade dependencies ([90f555f](https://github.com/pocket-id/pocket-id/commit/90f555f7c12ff07545f7cd1a1754a8c19f5a4978) by @stonith404)
- fix type error after version bump ([edb32d8](https://github.com/pocket-id/pocket-id/commit/edb32d82b2c138433d8eb17d5a6a19f4728ae2d4) by @stonith404)
- remove `breaking/**` push trigger from actions ([461293b](https://github.com/pocket-id/pocket-id/commit/461293ba1da4ddbff2c77f23a42487b63964e474) by @stonith404)
- update AAGUIDs ([#1177](https://github.com/pocket-id/pocket-id/pull/1177) by @github-actions[bot])
- preparation for merge into main branch ([#1113](https://github.com/pocket-id/pocket-id/pull/1113) by @stonith404)
- bump pnpm to version 10.27.0 ([#1183](https://github.com/pocket-id/pocket-id/pull/1183) by @kmendell)
- update forms and other areas to use new shadcn components ([#1115](https://github.com/pocket-id/pocket-id/pull/1115) by @kmendell)
- run formatter ([e4a8ca4](https://github.com/pocket-id/pocket-id/commit/e4a8ca476cc3c7e8d8cdc8de21b5d7d99d07f7a0) by @stonith404)
- upgrade dependencies ([4776b70](https://github.com/pocket-id/pocket-id/commit/4776b70d96f3dc291394dc79c941738bbe48199a) by @stonith404)
- change translation string in e2e tests ([ffb2ef9](https://github.com/pocket-id/pocket-id/commit/ffb2ef91bd7bbe78eb29e86cd3675b695e821498) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.16.0...v2.0.0
## v1.16.0
### Bug Fixes

View File

@@ -4,7 +4,7 @@ Pocket ID is a simple OIDC provider that allows users to authenticate with their
→ Try out the [Demo](https://demo.pocket-id.org)
<img src="https://github.com/user-attachments/assets/1e99ba44-76da-4b47-9b8a-dbe9b7f84512" width="1200"/>
<img src="https://github.com/user-attachments/assets/96ac549d-b897-404a-8811-f42b16ea58e2" width="1200"/>
The goal of Pocket ID is to be a simple and easy-to-use. There are other self-hosted OIDC providers like [Keycloak](https://www.keycloak.org/) or [ORY Hydra](https://www.ory.sh/hydra/) but they are often too complex for simple use cases.

View File

@@ -3,11 +3,11 @@ module github.com/pocket-id/pocket-id/backend
go 1.25
require (
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
github.com/aws/smithy-go v1.24.0
github.com/aws/aws-sdk-go-v2 v1.40.0
github.com/aws/aws-sdk-go-v2/config v1.32.2
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
github.com/aws/smithy-go v1.23.2
github.com/caarlos0/env/v11 v11.3.1
github.com/cenkalti/backoff/v5 v5.0.3
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -18,39 +18,39 @@ require (
github.com/gin-gonic/gin v1.11.0
github.com/glebarez/go-sqlite v1.22.0
github.com/glebarez/sqlite v1.11.0
github.com/go-co-op/gocron/v2 v2.19.0
github.com/go-co-op/gocron/v2 v2.18.1
github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-playground/validator/v10 v10.30.1
github.com/go-playground/validator/v10 v10.28.0
github.com/go-webauthn/webauthn v0.15.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/golang-migrate/migrate/v4 v4.19.0
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.3
github.com/lestrrat-go/httprc/v3 v3.0.1
github.com/lestrrat-go/jwx/v3 v3.0.12
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.1.1
github.com/spf13/cobra v1.10.2
github.com/oschwald/maxminddb-golang/v2 v2.1.0
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
go.opentelemetry.io/contrib/exporters/autoexport v0.64.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/log v0.15.0
go.opentelemetry.io/otel/metric v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/sdk/log v0.15.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
golang.org/x/crypto v0.46.0
golang.org/x/image v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.32.0
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/log v0.14.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.45.0
golang.org/x/image v0.33.0
golang.org/x/sync v0.18.0
golang.org/x/text v0.31.0
golang.org/x/time v0.14.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
@@ -58,33 +58,33 @@ require (
require (
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -92,18 +92,20 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-webauthn/x v0.1.27 // indirect
github.com/go-webauthn/x v0.1.26 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-github/v39 v39.2.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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.8.0 // indirect
github.com/jackc/pgx/v5 v5.7.6 // 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
@@ -115,57 +117,58 @@ require (
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/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.33 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect
github.com/quic-go/quic-go v0.57.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.61.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.67.4 // indirect
modernc.org/libc v1.67.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.42.2 // indirect
modernc.org/sqlite v1.40.1 // indirect
)

View File

@@ -6,44 +6,44 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
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/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
@@ -66,9 +66,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
@@ -98,8 +97,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/slog v1.2.0 h1:vAxZfr7knD1ZYK5+pMJLP52sZXIkJXkcRPa/0dx9hSk=
github.com/gin-contrib/slog v1.2.0/go.mod h1:vYK6YltmpsEFkO0zfRMLTKHrWS3DwUSn0TMpT+kMagI=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
@@ -112,8 +111,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-co-op/gocron/v2 v2.19.0 h1:OKf2y6LXPs/BgBI2fl8PxUpNAI1DA9Mg+hSeGOS38OU=
github.com/go-co-op/gocron/v2 v2.19.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-co-op/gocron/v2 v2.18.1 h1:VVxgAghLW1Q6VHi/rc+B0ZSpFoUVlWgkw09Yximvn58=
github.com/go-co-op/gocron/v2 v2.18.1/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -127,47 +126,50 @@ 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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
github.com/go-webauthn/x v0.1.27 h1:CLyuB8JGn9xvw0etBl4fnclcbPTwhKpN4Xg32zaSYnI=
github.com/go-webauthn/x v0.1.27/go.mod h1:KGYJQAPPgbpDKi4N7zKMGL+Iz6WgxKg3OlhVbPtuJXI=
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
github.com/google/go-tpm v0.9.7/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=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -178,8 +180,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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -226,10 +228,12 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs=
github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/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=
@@ -238,8 +242,8 @@ github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -263,15 +267,14 @@ 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.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
github.com/oschwald/maxminddb-golang/v2 v2.1.0 h1:2Iv7lmG9XtxuZA/jFAsd7LnZaC1E59pFsj5O/nU15pw=
github.com/oschwald/maxminddb-golang/v2 v2.1.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@@ -284,8 +287,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -295,8 +298,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -305,6 +308,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -316,62 +320,62 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
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=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 h1:7TYhBCu6Xz6vDJGNtEslWZLuuX2IJ/aH50hBY4MVeUg=
go.opentelemetry.io/contrib/bridges/prometheus v0.64.0/go.mod h1:tHQctZfAe7e4PBPGyt3kae6mQFXNpj+iiDJa3ithM50=
go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 h1:9pzPj3RFyKOxBAMkM2w84LpT+rdHam1XoFA+QhARiRw=
go.opentelemetry.io/contrib/exporters/autoexport v0.64.0/go.mod h1:hlVZx1btWH0XTfXpuGX9dsquB50s+tc3fYFOO5elo2M=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0 h1:7IKZbAYwlwLXAdu7SVPhzTjDjogWZxP4MIa7rovY+PU=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0/go.mod h1:+TF5nf3NIv2X8PGxqfYOaRnAoMM43rUA2C3XsN2DoWA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/exporters/prometheus v0.61.0 h1:cCyZS4dr67d30uDyh8etKM2QyDsQ4zC9ds3bdbrVoD0=
go.opentelemetry.io/otel/exporters/prometheus v0.61.0/go.mod h1:iivMuj3xpR2DkUrUya3TPS/Z9h3dz7h01GxU+fQBRNg=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 h1:0BSddrtQqLEylcErkeFrJBmwFzcqfQq9+/uxfTZq+HE=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0/go.mod h1:87sjYuAPzaRCtdd09GU5gM1U9wQLrrcYrm77mh5EBoc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 h1:5kSIJ0y8ckZZKoDhZHdVtcyjVi6rXyAwyaR8mp4zLbg=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0/go.mod h1:i+fIMHvcSQtsIY82/xgiVWRklrNt/O6QriHLjzGeY+s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo=
go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -380,58 +384,57 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -454,8 +457,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
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=
@@ -464,8 +467,8 @@ 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.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
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=

View File

@@ -24,8 +24,7 @@ func initApplicationImages(ctx context.Context, fileStorage storage.FileStorage)
// Previous versions of images
// If these are found, they are deleted
legacyImageHashes := imageHashMap{
"background.jpg": mustDecodeHex("138d510030ed845d1d74de34658acabff562d306476454369a60ab8ade31933f"),
"background.webp": mustDecodeHex("3fc436a66d6b872b01d96a4e75046c46b5c3e2daccd51e98ecdf98fd445599ab"),
"background.jpg": mustDecodeHex("138d510030ed845d1d74de34658acabff562d306476454369a60ab8ade31933f"),
}
sourceFiles, err := resources.FS.ReadDir("images")

View File

@@ -83,7 +83,6 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
controller.NewUserGroupController(apiGroup, authMiddleware, svc.userGroupService)
controller.NewCustomClaimController(apiGroup, authMiddleware, svc.customClaimService)
controller.NewVersionController(apiGroup, svc.versionService)
controller.NewScimController(apiGroup, authMiddleware, svc.scimService)
// Add test controller in non-production environments
if !common.EnvConfig.AppEnv.IsProduction() {
@@ -190,7 +189,6 @@ func initLogger(r *gin.Engine) {
"GET /api/application-images/logo",
"GET /api/application-images/background",
"GET /api/application-images/favicon",
"GET /api/application-images/email",
"GET /_app",
"GET /fonts",
"GET /healthz",

View File

@@ -12,24 +12,22 @@ import (
)
type services struct {
appConfigService *service.AppConfigService
appImagesService *service.AppImagesService
emailService *service.EmailService
geoLiteService *service.GeoLiteService
auditLogService *service.AuditLogService
jwtService *service.JwtService
webauthnService *service.WebAuthnService
scimService *service.ScimService
scimSchedulerService *service.ScimSchedulerService
userService *service.UserService
customClaimService *service.CustomClaimService
oidcService *service.OidcService
userGroupService *service.UserGroupService
ldapService *service.LdapService
apiKeyService *service.ApiKeyService
versionService *service.VersionService
fileStorage storage.FileStorage
appLockService *service.AppLockService
appConfigService *service.AppConfigService
appImagesService *service.AppImagesService
emailService *service.EmailService
geoLiteService *service.GeoLiteService
auditLogService *service.AuditLogService
jwtService *service.JwtService
webauthnService *service.WebAuthnService
userService *service.UserService
customClaimService *service.CustomClaimService
oidcService *service.OidcService
userGroupService *service.UserGroupService
ldapService *service.LdapService
apiKeyService *service.ApiKeyService
versionService *service.VersionService
fileStorage storage.FileStorage
appLockService *service.AppLockService
}
// Initializes all services
@@ -72,11 +70,6 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, fileStorage)
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService, fileStorage)
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
svc.scimService = service.NewScimService(db, httpClient)
svc.scimSchedulerService, err = service.NewScimSchedulerService(ctx, svc.scimService)
if err != nil {
return nil, fmt.Errorf("failed to create SCIM scheduler service: %w", err)
}
svc.versionService = service.NewVersionService(httpClient)

View File

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

View File

@@ -38,20 +38,33 @@ const (
)
type EnvConfigSchema struct {
AppEnv AppEnv `env:"APP_ENV" options:"toLower"`
EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"`
AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"`
DbProvider DbProvider
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
TrustProxy bool `env:"TRUST_PROXY"`
AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"`
AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"`
AllowDowngrade bool `env:"ALLOW_DOWNGRADE"`
InternalAppURL string `env:"INTERNAL_APP_URL"`
UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"`
AppEnv AppEnv `env:"APP_ENV" options:"toLower"`
LogLevel string `env:"LOG_LEVEL" options:"toLower"`
LogJSON bool `env:"LOG_JSON"`
AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"`
DbProvider DbProvider
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"`
Port string `env:"PORT"`
Host string `env:"HOST" options:"toLower"`
UnixSocket string `env:"UNIX_SOCKET"`
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"`
MetricsEnabled bool `env:"METRICS_ENABLED"`
TracingEnabled bool `env:"TRACING_ENABLED"`
TrustProxy bool `env:"TRUST_PROXY"`
AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"`
AllowDowngrade bool `env:"ALLOW_DOWNGRADE"`
InternalAppURL string `env:"INTERNAL_APP_URL"`
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
FileBackend string `env:"FILE_BACKEND" options:"toLower"`
UploadPath string `env:"UPLOAD_PATH"`
FileBackend string `env:"FILE_BACKEND" options:"toLower"`
UploadPath string `env:"UPLOAD_PATH"`
S3Bucket string `env:"S3_BUCKET"`
S3Region string `env:"S3_REGION"`
S3Endpoint string `env:"S3_ENDPOINT"`
@@ -59,21 +72,6 @@ type EnvConfigSchema struct {
S3SecretAccessKey string `env:"S3_SECRET_ACCESS_KEY"`
S3ForcePathStyle bool `env:"S3_FORCE_PATH_STYLE"`
S3DisableDefaultIntegrityChecks bool `env:"S3_DISABLE_DEFAULT_INTEGRITY_CHECKS"`
Port string `env:"PORT"`
Host string `env:"HOST" options:"toLower"`
UnixSocket string `env:"UNIX_SOCKET"`
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
LogLevel string `env:"LOG_LEVEL" options:"toLower"`
MetricsEnabled bool `env:"METRICS_ENABLED"`
TracingEnabled bool `env:"TRACING_ENABLED"`
LogJSON bool `env:"LOG_JSON"`
}
var EnvConfig = defaultConfig()
@@ -88,16 +86,15 @@ func init() {
func defaultConfig() EnvConfigSchema {
return EnvConfigSchema{
AppEnv: AppEnvProduction,
LogLevel: "info",
DbProvider: "sqlite",
FileBackend: "filesystem",
AuditLogRetentionDays: 90,
AppURL: AppUrl,
Port: "1411",
Host: "0.0.0.0",
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
GeoLiteDBUrl: MaxMindGeoLiteCityUrl,
AppEnv: AppEnvProduction,
LogLevel: "info",
DbProvider: "sqlite",
FileBackend: "filesystem",
AppURL: AppUrl,
Port: "1411",
Host: "0.0.0.0",
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
GeoLiteDBUrl: MaxMindGeoLiteCityUrl,
}
}
@@ -167,7 +164,6 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
switch config.FileBackend {
case "s3", "database":
// All good, these are valid values
case "", "filesystem":
if config.UploadPath == "" {
config.UploadPath = defaultFsUploadPath
@@ -195,10 +191,6 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
}
if config.AuditLogRetentionDays <= 0 {
return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0")
}
return nil
}

View File

@@ -129,41 +129,6 @@ func TestParseEnvConfig(t *testing.T) {
assert.False(t, EnvConfig.AnalyticsDisabled)
})
t.Run("should default audit log retention days to 90", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_PROVIDER", "sqlite")
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
t.Setenv("APP_URL", "http://localhost:3000")
err := parseEnvConfig()
require.NoError(t, err)
assert.Equal(t, 90, EnvConfig.AuditLogRetentionDays)
})
t.Run("should parse audit log retention days override", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_PROVIDER", "sqlite")
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
t.Setenv("APP_URL", "http://localhost:3000")
t.Setenv("AUDIT_LOG_RETENTION_DAYS", "365")
err := parseEnvConfig()
require.NoError(t, err)
assert.Equal(t, 365, EnvConfig.AuditLogRetentionDays)
})
t.Run("should fail when AUDIT_LOG_RETENTION_DAYS is non-positive", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_PROVIDER", "sqlite")
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
t.Setenv("APP_URL", "http://localhost:3000")
t.Setenv("AUDIT_LOG_RETENTION_DAYS", "0")
err := parseAndValidateEnvConfig(t)
require.Error(t, err)
assert.ErrorContains(t, err, "AUDIT_LOG_RETENTION_DAYS must be greater than 0")
})
t.Run("should parse string environment variables correctly", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_CONNECTION_STRING", "postgres://test")

View File

@@ -38,13 +38,6 @@ type TokenInvalidOrExpiredError struct{}
func (e *TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" }
func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return 400 }
type DeviceCodeInvalid struct{}
func (e *DeviceCodeInvalid) Error() string {
return "one time access code must be used on the device it was generated for"
}
func (e *DeviceCodeInvalid) HttpStatusCode() int { return 400 }
type TokenInvalidError struct{}
func (e *TokenInvalidError) Error() string {

View File

@@ -23,13 +23,11 @@ func NewAppImagesController(
}
group.GET("/application-images/logo", controller.getLogoHandler)
group.GET("/application-images/email", controller.getEmailLogoHandler)
group.GET("/application-images/background", controller.getBackgroundImageHandler)
group.GET("/application-images/favicon", controller.getFaviconHandler)
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
group.PUT("/application-images/email", authMiddleware.Add(), controller.updateEmailLogoHandler)
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
@@ -61,18 +59,6 @@ func (c *AppImagesController) getLogoHandler(ctx *gin.Context) {
c.getImage(ctx, imageName)
}
// getEmailLogoHandler godoc
// @Summary Get email logo image
// @Description Get the email logo image for use in emails
// @Tags Application Images
// @Produce image/png
// @Produce image/jpeg
// @Success 200 {file} binary "Email logo image"
// @Router /api/application-images/email [get]
func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) {
c.getImage(ctx, "logoEmail")
}
// getBackgroundImageHandler godoc
// @Summary Get background image
// @Description Get the background image for the application
@@ -138,37 +124,6 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
ctx.Status(http.StatusNoContent)
}
// updateEmailLogoHandler godoc
// @Summary Update email logo
// @Description Update the email logo for use in emails
// @Tags Application Images
// @Accept multipart/form-data
// @Param file formData file true "Email logo image file"
// @Success 204 "No Content"
// @Router /api/application-images/email [put]
func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) {
file, err := ctx.FormFile("file")
if err != nil {
_ = ctx.Error(err)
return
}
fileType := utils.GetFileExtension(file.Filename)
mimeType := utils.GetImageMimeType(fileType)
if mimeType != "image/png" && mimeType != "image/jpeg" {
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".png or .jpg/jpeg"})
return
}
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "logoEmail"); err != nil {
_ = ctx.Error(err)
return
}
ctx.Status(http.StatusNoContent)
}
// updateBackgroundImageHandler godoc
// @Summary Update background image
// @Description Update the application background image

View File

@@ -63,8 +63,12 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAccessibleClientsHandler)
group.GET("/oidc/clients/:id/scim-service-provider", authMiddleware.Add(), oc.getClientScimServiceProviderHandler)
// OIDC API (Resource Server) routes
group.POST("/oidc/apis", authMiddleware.Add(), oc.createAPIHandler)
group.GET("/oidc/apis", authMiddleware.Add(), oc.listAPIsHandler)
group.GET("/oidc/apis/:id", authMiddleware.Add(), oc.getAPIHandler)
group.POST("/oidc/apis/:id", authMiddleware.Add(), oc.updateAPIHandler)
group.DELETE("/oidc/apis/:id", authMiddleware.Add(), oc.deleteAPIHandler)
}
type OidcController struct {
@@ -848,28 +852,150 @@ func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
c.JSON(http.StatusOK, preview)
}
// getClientScimServiceProviderHandler godoc
// @Summary Get SCIM service provider
// @Description Get the SCIM service provider configuration for an OIDC client
// createAPIHandler godoc
// @Summary Create OIDC API
// @Description Create a new OIDC API (resource server)
// @Tags OIDC
// @Accept json
// @Produce json
// @Param id path string true "Client ID"
// @Success 200 {object} dto.ScimServiceProviderDTO "SCIM service provider configuration"
// @Router /api/oidc/clients/{id}/scim-service-provider [get]
func (oc *OidcController) getClientScimServiceProviderHandler(c *gin.Context) {
clientID := c.Param("id")
provider, err := oc.oidcService.GetClientScimServiceProvider(c.Request.Context(), clientID)
// @Param api body dto.OidcAPICreateDto true "API information"
// @Success 201 {object} dto.OidcAPIDto "Created API"
// @Router /api/oidc/apis [post]
func (oc *OidcController) createAPIHandler(c *gin.Context) {
var input dto.OidcAPICreateDto
err := c.ShouldBindJSON(&input)
if err != nil {
_ = c.Error(err)
return
}
var providerDto dto.ScimServiceProviderDTO
if err := dto.MapStruct(provider, &providerDto); err != nil {
api, err := oc.oidcService.CreateAPI(c.Request.Context(), input)
if err != nil {
_ = c.Error(err)
return
}
c.JSON(http.StatusOK, providerDto)
var apiDto dto.OidcAPIDto
err = dto.MapStruct(api, &apiDto)
if err != nil {
_ = c.Error(err)
return
}
c.JSON(http.StatusCreated, apiDto)
}
// listAPIsHandler godoc
// @Summary List OIDC APIs
// @Description Get a paginated list of OIDC APIs (resource servers)
// @Tags OIDC
// @Param search query string false "Search term to filter APIs by name"
// @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.OidcAPIDto]
// @Router /api/oidc/apis [get]
func (oc *OidcController) listAPIsHandler(c *gin.Context) {
searchTerm := c.Query("search")
listRequestOptions := utils.ParseListRequestOptions(c)
apis, pagination, err := oc.oidcService.ListAPIs(c.Request.Context(), searchTerm, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
}
apisDto := make([]dto.OidcAPIDto, len(apis))
for i, api := range apis {
var apiDto dto.OidcAPIDto
err = dto.MapStruct(api, &apiDto)
if err != nil {
_ = c.Error(err)
return
}
apisDto[i] = apiDto
}
c.JSON(http.StatusOK, dto.Paginated[dto.OidcAPIDto]{
Data: apisDto,
Pagination: pagination,
})
}
// getAPIHandler godoc
// @Summary Get OIDC API
// @Description Get detailed information about an OIDC API (resource server)
// @Tags OIDC
// @Produce json
// @Param id path string true "API ID"
// @Success 200 {object} dto.OidcAPIDto "API information"
// @Router /api/oidc/apis/{id} [get]
func (oc *OidcController) getAPIHandler(c *gin.Context) {
apiID := c.Param("id")
api, err := oc.oidcService.GetAPI(c.Request.Context(), apiID)
if err != nil {
_ = c.Error(err)
return
}
var apiDto dto.OidcAPIDto
err = dto.MapStruct(api, &apiDto)
if err != nil {
_ = c.Error(err)
return
}
c.JSON(http.StatusOK, apiDto)
}
// updateAPIHandler godoc
// @Summary Update OIDC API
// @Description Update an existing OIDC API (resource server)
// @Tags OIDC
// @Accept json
// @Produce json
// @Param id path string true "API ID"
// @Param api body dto.OidcAPIUpdateDto true "API information"
// @Success 200 {object} dto.OidcAPIDto "Updated API"
// @Router /api/oidc/apis/{id} [post]
func (oc *OidcController) updateAPIHandler(c *gin.Context) {
var input dto.OidcAPIUpdateDto
err := c.ShouldBindJSON(&input)
if err != nil {
_ = c.Error(err)
return
}
api, err := oc.oidcService.UpdateAPI(c.Request.Context(), c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
}
var apiDto dto.OidcAPIDto
err = dto.MapStruct(api, &apiDto)
if err != nil {
_ = c.Error(err)
return
}
c.JSON(http.StatusOK, apiDto)
}
// deleteAPIHandler godoc
// @Summary Delete OIDC API
// @Description Delete an OIDC API (resource server) by ID
// @Tags OIDC
// @Param id path string true "API ID"
// @Success 204 "No Content"
// @Router /api/oidc/apis/{id} [delete]
func (oc *OidcController) deleteAPIHandler(c *gin.Context) {
err := oc.oidcService.DeleteAPI(c.Request.Context(), c.Param("id"))
if err != nil {
_ = c.Error(err)
return
}
c.Status(http.StatusNoContent)
}

View File

@@ -1,122 +0,0 @@
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
func NewScimController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, scimService *service.ScimService) {
ugc := ScimController{
scimService: scimService,
}
group.POST("/scim/service-provider", authMiddleware.Add(), ugc.createServiceProviderHandler)
group.POST("/scim/service-provider/:id/sync", authMiddleware.Add(), ugc.syncServiceProviderHandler)
group.PUT("/scim/service-provider/:id", authMiddleware.Add(), ugc.updateServiceProviderHandler)
group.DELETE("/scim/service-provider/:id", authMiddleware.Add(), ugc.deleteServiceProviderHandler)
}
type ScimController struct {
scimService *service.ScimService
}
// syncServiceProviderHandler godoc
// @Summary Sync SCIM service provider
// @Description Trigger synchronization for a SCIM service provider
// @Tags SCIM
// @Param id path string true "Service Provider ID"
// @Success 200 "OK"
// @Router /api/scim/service-provider/{id}/sync [post]
func (c *ScimController) syncServiceProviderHandler(ctx *gin.Context) {
err := c.scimService.SyncServiceProvider(ctx.Request.Context(), ctx.Param("id"))
if err != nil {
_ = ctx.Error(err)
return
}
ctx.Status(http.StatusOK)
}
// createServiceProviderHandler godoc
// @Summary Create SCIM service provider
// @Description Create a new SCIM service provider
// @Tags SCIM
// @Accept json
// @Produce json
// @Param serviceProvider body dto.ScimServiceProviderCreateDTO true "SCIM service provider information"
// @Success 201 {object} dto.ScimServiceProviderDTO "Created SCIM service provider"
// @Router /api/scim/service-provider [post]
func (c *ScimController) createServiceProviderHandler(ctx *gin.Context) {
var input dto.ScimServiceProviderCreateDTO
if err := ctx.ShouldBindJSON(&input); err != nil {
_ = ctx.Error(err)
return
}
provider, err := c.scimService.CreateServiceProvider(ctx.Request.Context(), &input)
if err != nil {
_ = ctx.Error(err)
return
}
var providerDTO dto.ScimServiceProviderDTO
if err := dto.MapStruct(provider, &providerDTO); err != nil {
_ = ctx.Error(err)
return
}
ctx.JSON(http.StatusCreated, providerDTO)
}
// updateServiceProviderHandler godoc
// @Summary Update SCIM service provider
// @Description Update an existing SCIM service provider
// @Tags SCIM
// @Accept json
// @Produce json
// @Param id path string true "Service Provider ID"
// @Param serviceProvider body dto.ScimServiceProviderCreateDTO true "SCIM service provider information"
// @Success 200 {object} dto.ScimServiceProviderDTO "Updated SCIM service provider"
// @Router /api/scim/service-provider/{id} [put]
func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) {
var input dto.ScimServiceProviderCreateDTO
if err := ctx.ShouldBindJSON(&input); err != nil {
_ = ctx.Error(err)
return
}
provider, err := c.scimService.UpdateServiceProvider(ctx.Request.Context(), ctx.Param("id"), &input)
if err != nil {
_ = ctx.Error(err)
return
}
var providerDTO dto.ScimServiceProviderDTO
if err := dto.MapStruct(provider, &providerDTO); err != nil {
_ = ctx.Error(err)
return
}
ctx.JSON(http.StatusOK, providerDTO)
}
// deleteServiceProviderHandler godoc
// @Summary Delete SCIM service provider
// @Description Delete a SCIM service provider by ID
// @Tags SCIM
// @Param id path string true "Service Provider ID"
// @Success 204 "No Content"
// @Router /api/scim/service-provider/{id} [delete]
func (c *ScimController) deleteServiceProviderHandler(ctx *gin.Context) {
err := c.scimService.DeleteServiceProvider(ctx.Request.Context(), ctx.Param("id"))
if err != nil {
_ = ctx.Error(err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@@ -72,7 +72,7 @@ type UserController struct {
// @Description Retrieve all groups a specific user belongs to
// @Tags Users,User Groups
// @Param id path string true "User ID"
// @Success 200 {array} dto.UserGroupDto
// @Success 200 {array} dto.UserGroupDtoWithUsers
// @Router /api/users/{id}/groups [get]
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
userID := c.Param("id")
@@ -82,7 +82,7 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
return
}
var groupsDto []dto.UserGroupDto
var groupsDto []dto.UserGroupDtoWithUsers
if err := dto.MapStructList(groups, &groupsDto); err != nil {
_ = c.Error(err)
return
@@ -391,13 +391,12 @@ func (uc *UserController) RequestOneTimeAccessEmailAsUnauthenticatedUserHandler(
return
}
deviceToken, err := uc.userService.RequestOneTimeAccessEmailAsUnauthenticatedUser(c.Request.Context(), input.Email, input.RedirectPath)
err := uc.userService.RequestOneTimeAccessEmailAsUnauthenticatedUser(c.Request.Context(), input.Email, input.RedirectPath)
if err != nil {
_ = c.Error(err)
return
}
cookie.AddDeviceTokenCookie(c, deviceToken)
c.Status(http.StatusNoContent)
}
@@ -441,8 +440,7 @@ func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context
// @Success 200 {object} dto.UserDto
// @Router /api/one-time-access-token/{token} [post]
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
deviceToken, _ := c.Cookie(cookie.DeviceTokenCookieName)
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Request.Context(), c.Param("token"), deviceToken, c.ClientIP(), c.Request.UserAgent())
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Request.Context(), c.Param("token"), c.ClientIP(), c.Request.UserAgent())
if err != nil {
_ = c.Error(err)
return
@@ -545,7 +543,7 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
ttl = defaultSignupTokenDuration
}
signupToken, err := uc.userService.CreateSignupToken(c.Request.Context(), ttl, input.UsageLimit, input.UserGroupIDs)
signupToken, err := uc.userService.CreateSignupToken(c.Request.Context(), ttl, input.UsageLimit)
if err != nil {
_ = c.Error(err)
return

View File

@@ -28,7 +28,6 @@ func NewUserGroupController(group *gin.RouterGroup, authMiddleware *middleware.A
userGroupsGroup.PUT("/:id", ugc.update)
userGroupsGroup.DELETE("/:id", ugc.delete)
userGroupsGroup.PUT("/:id/users", ugc.updateUsers)
userGroupsGroup.PUT("/:id/allowed-oidc-clients", ugc.updateAllowedOidcClients)
}
}
@@ -45,7 +44,7 @@ type UserGroupController struct {
// @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.UserGroupMinimalDto]
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
// @Router /api/user-groups [get]
func (ugc *UserGroupController) list(c *gin.Context) {
searchTerm := c.Query("search")
@@ -58,9 +57,9 @@ func (ugc *UserGroupController) list(c *gin.Context) {
}
// Map the user groups to DTOs
var groupsDto = make([]dto.UserGroupMinimalDto, len(groups))
var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups))
for i, group := range groups {
var groupDto dto.UserGroupMinimalDto
var groupDto dto.UserGroupDtoWithUserCount
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
@@ -73,7 +72,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
groupsDto[i] = groupDto
}
c.JSON(http.StatusOK, dto.Paginated[dto.UserGroupMinimalDto]{
c.JSON(http.StatusOK, dto.Paginated[dto.UserGroupDtoWithUserCount]{
Data: groupsDto,
Pagination: pagination,
})
@@ -86,7 +85,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
// @Accept json
// @Produce json
// @Param id path string true "User Group ID"
// @Success 200 {object} dto.UserGroupDto
// @Success 200 {object} dto.UserGroupDtoWithUsers
// @Router /api/user-groups/{id} [get]
func (ugc *UserGroupController) get(c *gin.Context) {
group, err := ugc.UserGroupService.Get(c.Request.Context(), c.Param("id"))
@@ -95,7 +94,7 @@ func (ugc *UserGroupController) get(c *gin.Context) {
return
}
var groupDto dto.UserGroupDto
var groupDto dto.UserGroupDtoWithUsers
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
@@ -111,7 +110,7 @@ func (ugc *UserGroupController) get(c *gin.Context) {
// @Accept json
// @Produce json
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
// @Success 201 {object} dto.UserGroupDto "Created user group"
// @Success 201 {object} dto.UserGroupDtoWithUsers "Created user group"
// @Router /api/user-groups [post]
func (ugc *UserGroupController) create(c *gin.Context) {
var input dto.UserGroupCreateDto
@@ -126,7 +125,7 @@ func (ugc *UserGroupController) create(c *gin.Context) {
return
}
var groupDto dto.UserGroupDto
var groupDto dto.UserGroupDtoWithUsers
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
@@ -143,7 +142,7 @@ func (ugc *UserGroupController) create(c *gin.Context) {
// @Produce json
// @Param id path string true "User Group ID"
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
// @Success 200 {object} dto.UserGroupDto "Updated user group"
// @Success 200 {object} dto.UserGroupDtoWithUsers "Updated user group"
// @Router /api/user-groups/{id} [put]
func (ugc *UserGroupController) update(c *gin.Context) {
var input dto.UserGroupCreateDto
@@ -158,7 +157,7 @@ func (ugc *UserGroupController) update(c *gin.Context) {
return
}
var groupDto dto.UserGroupDto
var groupDto dto.UserGroupDtoWithUsers
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
@@ -193,7 +192,7 @@ func (ugc *UserGroupController) delete(c *gin.Context) {
// @Produce json
// @Param id path string true "User Group ID"
// @Param users body dto.UserGroupUpdateUsersDto true "List of user IDs to assign to this group"
// @Success 200 {object} dto.UserGroupDto
// @Success 200 {object} dto.UserGroupDtoWithUsers
// @Router /api/user-groups/{id}/users [put]
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
var input dto.UserGroupUpdateUsersDto
@@ -208,7 +207,7 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) {
return
}
var groupDto dto.UserGroupDto
var groupDto dto.UserGroupDtoWithUsers
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
@@ -216,35 +215,3 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) {
c.JSON(http.StatusOK, groupDto)
}
// updateAllowedOidcClients godoc
// @Summary Update allowed OIDC clients
// @Description Update the OIDC clients allowed for a specific user group
// @Tags OIDC
// @Accept json
// @Produce json
// @Param id path string true "User Group ID"
// @Param groups body dto.UserGroupUpdateAllowedOidcClientsDto true "OIDC client IDs to allow"
// @Success 200 {object} dto.UserGroupDto "Updated user group"
// @Router /api/user-groups/{id}/allowed-oidc-clients [put]
func (ugc *UserGroupController) updateAllowedOidcClients(c *gin.Context) {
var input dto.UserGroupUpdateAllowedOidcClientsDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
}
userGroup, err := ugc.UserGroupService.UpdateAllowedOidcClient(c.Request.Context(), c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
}
var userGroupDto dto.UserGroupDto
if err := dto.MapStruct(userGroup, &userGroupDto); err != nil {
_ = c.Error(err)
return
}
c.JSON(http.StatusOK, userGroupDto)
}

View File

@@ -18,12 +18,11 @@ type OidcClientDto struct {
IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled"`
Credentials OidcClientCredentialsDto `json:"credentials"`
IsGroupRestricted bool `json:"isGroupRestricted"`
}
type OidcClientWithAllowedUserGroupsDto struct {
OidcClientDto
AllowedUserGroups []UserGroupMinimalDto `json:"allowedUserGroups"`
AllowedUserGroups []UserGroupDtoWithUserCount `json:"allowedUserGroups"`
}
type OidcClientWithAllowedGroupsCountDto struct {
@@ -44,7 +43,6 @@ type OidcClientUpdateDto struct {
HasDarkLogo bool `json:"hasDarkLogo"`
LogoURL *string `json:"logoUrl"`
DarkLogoURL *string `json:"darkLogoUrl"`
IsGroupRestricted bool `json:"isGroupRestricted"`
}
type OidcClientCreateDto struct {
@@ -180,3 +178,26 @@ type AccessibleOidcClientDto struct {
OidcClientMetaDataDto
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
}
type OidcAPIDto struct {
ID string `json:"id"`
Name string `json:"name"`
Identifier string `json:"identifier"`
Permissions []OidcAPIPermissionDto `json:"permissions"`
CreatedAt datatype.DateTime `json:"createdAt"`
}
type OidcAPIPermissionDto struct {
Name string `json:"name"`
Description string `json:"description"`
}
type OidcAPICreateDto struct {
Name string `json:"name" binding:"required,max=100" unorm:"nfc"`
}
type OidcAPIUpdateDto struct {
Name string `json:"name" binding:"required,max=100" unorm:"nfc"`
Identifier string `json:"identifier" binding:"omitempty,url,max=255"`
Permissions []OidcAPIPermissionDto `json:"permissions" binding:"omitempty,dive"`
}

View File

@@ -1,96 +0,0 @@
package dto
import (
"time"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
)
type ScimServiceProviderDTO struct {
ID string `json:"id"`
Endpoint string `json:"endpoint"`
Token string `json:"token"`
LastSyncedAt *datatype.DateTime `json:"lastSyncedAt"`
OidcClient OidcClientMetaDataDto `json:"oidcClient"`
CreatedAt datatype.DateTime `json:"createdAt"`
}
type ScimServiceProviderCreateDTO struct {
Endpoint string `json:"endpoint" binding:"required,url"`
Token string `json:"token"`
OidcClientID string `json:"oidcClientId" binding:"required"`
}
type ScimUser struct {
ScimResourceData
UserName string `json:"userName"`
Name *ScimName `json:"name,omitempty"`
Display string `json:"displayName,omitempty"`
Active bool `json:"active"`
Emails []ScimEmail `json:"emails,omitempty"`
}
type ScimName struct {
GivenName string `json:"givenName,omitempty"`
FamilyName string `json:"familyName,omitempty"`
}
type ScimEmail struct {
Value string `json:"value"`
Primary bool `json:"primary,omitempty"`
}
type ScimGroup struct {
ScimResourceData
Display string `json:"displayName"`
Members []ScimGroupMember `json:"members,omitempty"`
}
type ScimGroupMember struct {
Value string `json:"value"`
}
type ScimListResponse[T any] struct {
Resources []T `json:"Resources"`
TotalResults int `json:"totalResults"`
StartIndex int `json:"startIndex"`
ItemsPerPage int `json:"itemsPerPage"`
}
type ScimResourceData struct {
ID string `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
Schemas []string `json:"schemas"`
Meta ScimResourceMeta `json:"meta,omitempty"`
}
type ScimResourceMeta struct {
Location string `json:"location,omitempty"`
ResourceType string `json:"resourceType,omitempty"`
Created time.Time `json:"created,omitempty"`
LastModified time.Time `json:"lastModified,omitempty"`
Version string `json:"version,omitempty"`
}
func (r ScimResourceData) GetID() string {
return r.ID
}
func (r ScimResourceData) GetExternalID() string {
return r.ExternalID
}
func (r ScimResourceData) GetSchemas() []string {
return r.Schemas
}
func (r ScimResourceData) GetMeta() ScimResourceMeta {
return r.Meta
}
type ScimResource interface {
GetID() string
GetExternalID() string
GetSchemas() []string
GetMeta() ScimResourceMeta
}

View File

@@ -6,17 +6,15 @@ import (
)
type SignupTokenCreateDto struct {
TTL utils.JSONDuration `json:"ttl" binding:"required,ttl"`
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
UserGroupIDs []string `json:"userGroupIds"`
TTL utils.JSONDuration `json:"ttl" binding:"required,ttl"`
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
}
type SignupTokenDto struct {
ID string `json:"id"`
Token string `json:"token"`
ExpiresAt datatype.DateTime `json:"expiresAt"`
UsageLimit int `json:"usageLimit"`
UsageCount int `json:"usageCount"`
UserGroups []UserGroupMinimalDto `json:"userGroups"`
CreatedAt datatype.DateTime `json:"createdAt"`
ID string `json:"id"`
Token string `json:"token"`
ExpiresAt datatype.DateTime `json:"expiresAt"`
UsageLimit int `json:"usageLimit"`
UsageCount int `json:"usageCount"`
CreatedAt datatype.DateTime `json:"createdAt"`
}

View File

@@ -8,31 +8,30 @@ import (
)
type UserDto struct {
ID string `json:"id"`
Username string `json:"username"`
Email *string `json:"email" `
FirstName string `json:"firstName"`
LastName *string `json:"lastName"`
DisplayName string `json:"displayName"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
CustomClaims []CustomClaimDto `json:"customClaims"`
UserGroups []UserGroupMinimalDto `json:"userGroups"`
LdapID *string `json:"ldapId"`
Disabled bool `json:"disabled"`
ID string `json:"id"`
Username string `json:"username"`
Email *string `json:"email" `
FirstName string `json:"firstName"`
LastName *string `json:"lastName"`
DisplayName string `json:"displayName"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
CustomClaims []CustomClaimDto `json:"customClaims"`
UserGroups []UserGroupDto `json:"userGroups"`
LdapID *string `json:"ldapId"`
Disabled bool `json:"disabled"`
}
type UserCreateDto struct {
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
Disabled bool `json:"disabled"`
UserGroupIds []string `json:"userGroupIds"`
LdapID string `json:"-"`
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
Disabled bool `json:"disabled"`
LdapID string `json:"-"`
}
func (u UserCreateDto) Validate() error {

View File

@@ -8,17 +8,25 @@ import (
)
type UserGroupDto struct {
ID string `json:"id"`
FriendlyName string `json:"friendlyName"`
Name string `json:"name"`
CustomClaims []CustomClaimDto `json:"customClaims"`
LdapID *string `json:"ldapId"`
CreatedAt datatype.DateTime `json:"createdAt"`
Users []UserDto `json:"users"`
AllowedOidcClients []OidcClientMetaDataDto `json:"allowedOidcClients"`
ID string `json:"id"`
FriendlyName string `json:"friendlyName"`
Name string `json:"name"`
CustomClaims []CustomClaimDto `json:"customClaims"`
LdapID *string `json:"ldapId"`
CreatedAt datatype.DateTime `json:"createdAt"`
}
type UserGroupMinimalDto struct {
type UserGroupDtoWithUsers struct {
ID string `json:"id"`
FriendlyName string `json:"friendlyName"`
Name string `json:"name"`
CustomClaims []CustomClaimDto `json:"customClaims"`
Users []UserDto `json:"users"`
LdapID *string `json:"ldapId"`
CreatedAt datatype.DateTime `json:"createdAt"`
}
type UserGroupDtoWithUserCount struct {
ID string `json:"id"`
FriendlyName string `json:"friendlyName"`
Name string `json:"name"`
@@ -28,10 +36,6 @@ type UserGroupMinimalDto struct {
CreatedAt datatype.DateTime `json:"createdAt"`
}
type UserGroupUpdateAllowedOidcClientsDto struct {
OidcClientIDs []string `json:"oidcClientIds" binding:"required"`
}
type UserGroupCreateDto struct {
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50" unorm:"nfc"`
Name string `json:"name" binding:"required,min=2,max=255" unorm:"nfc"`

View File

@@ -10,7 +10,6 @@ import (
"github.com/go-co-op/gocron/v2"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
)
@@ -120,13 +119,11 @@ func (j *DbCleanupJobs) clearReauthenticationTokens(ctx context.Context) error {
return nil
}
// ClearAuditLogs deletes audit logs older than the configured retention window
// ClearAuditLogs deletes audit logs older than 90 days
func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
cutoff := time.Now().AddDate(0, 0, -common.EnvConfig.AuditLogRetentionDays)
st := j.db.
WithContext(ctx).
Delete(&model.AuditLog{}, "created_at < ?", datatype.DateTime(cutoff))
Delete(&model.AuditLog{}, "created_at < ?", datatype.DateTime(time.Now().AddDate(0, 0, -90)))
if st.Error != nil {
return fmt.Errorf("failed to delete old audit logs: %w", st.Error)
}

View File

@@ -58,7 +58,6 @@ type OidcClient struct {
RequiresReauthentication bool `sortable:"true" filterable:"true"`
Credentials OidcClientCredentials
LaunchURL *string
IsGroupRestricted bool `sortable:"true" filterable:"true"`
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
CreatedByID *string
@@ -152,3 +151,33 @@ type OidcDeviceCode struct {
ClientID string
Client OidcClient
}
type OidcAPI struct {
Base
Name string `sortable:"true"`
Identifier string `sortable:"true"`
Data OidcAPIData `gorm:"type:text"`
}
func (a OidcAPI) DefaultIdentifier() string {
return "api://" + a.Identifier
}
//nolint:recvcheck
type OidcAPIData struct {
Permissions []OidcAPIPermission `json:"permissions"`
}
func (p *OidcAPIData) Scan(value any) error {
return utils.UnmarshalJSONFromDatabase(p, value)
}
func (p OidcAPIData) Value() (driver.Value, error) {
return json.Marshal(p)
}
type OidcAPIPermission struct {
Name string `json:"name"`
Description string `json:"description"`
}

View File

@@ -1,14 +0,0 @@
package model
import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
type ScimServiceProvider struct {
Base
Endpoint string `sortable:"true"`
Token datatype.EncryptedString
LastSyncedAt *datatype.DateTime `sortable:"true"`
OidcClientID string
OidcClient OidcClient `gorm:"foreignKey:OidcClientID;references:ID;"`
}

View File

@@ -13,7 +13,6 @@ type SignupToken struct {
ExpiresAt datatype.DateTime `json:"expiresAt" sortable:"true"`
UsageLimit int `json:"usageLimit" sortable:"true"`
UsageCount int `json:"usageCount" sortable:"true"`
UserGroups []UserGroup `gorm:"many2many:signup_tokens_user_groups;"`
}
func (st *SignupToken) IsExpired() bool {

View File

@@ -1,91 +0,0 @@
package datatype
import (
"crypto/sha256"
"database/sql/driver"
"encoding/base64"
"fmt"
"io"
"github.com/pocket-id/pocket-id/backend/internal/common"
cryptoutils "github.com/pocket-id/pocket-id/backend/internal/utils/crypto"
"golang.org/x/crypto/hkdf"
)
const encryptedStringAAD = "encrypted_string"
var encStringKey []byte
// EncryptedString stores plaintext in memory and persists encrypted data in the database.
type EncryptedString string //nolint:recvcheck
func (e *EncryptedString) Scan(value any) error {
if value == nil {
*e = ""
return nil
}
var raw string
switch v := value.(type) {
case string:
raw = v
case []byte:
raw = string(v)
default:
return fmt.Errorf("unexpected type for EncryptedString: %T", value)
}
if raw == "" {
*e = ""
return nil
}
encBytes, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return fmt.Errorf("failed to decode encrypted string: %w", err)
}
decBytes, err := cryptoutils.Decrypt(encStringKey, encBytes, []byte(encryptedStringAAD))
if err != nil {
return fmt.Errorf("failed to decrypt encrypted string: %w", err)
}
*e = EncryptedString(decBytes)
return nil
}
func (e EncryptedString) Value() (driver.Value, error) {
if e == "" {
return "", nil
}
encBytes, err := cryptoutils.Encrypt(encStringKey, []byte(e), []byte(encryptedStringAAD))
if err != nil {
return nil, fmt.Errorf("failed to encrypt string: %w", err)
}
return base64.StdEncoding.EncodeToString(encBytes), nil
}
func (e EncryptedString) String() string {
return string(e)
}
func deriveEncryptedStringKey(master []byte) ([]byte, error) {
const info = "pocketid/encrypted_string"
r := hkdf.New(sha256.New, master, nil, []byte(info))
key := make([]byte, 32)
if _, err := io.ReadFull(r, key); err != nil {
return nil, err
}
return key, nil
}
func init() {
key, err := deriveEncryptedStringKey(common.EnvConfig.EncryptionKey)
if err != nil {
panic(fmt.Sprintf("failed to derive encrypted string key: %v", err))
}
encStringKey = key
}

View File

@@ -2,7 +2,6 @@ package model
import (
"strings"
"time"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
@@ -23,7 +22,6 @@ type User struct {
Locale *string
LdapID *string
Disabled bool `sortable:"true" filterable:"true"`
UpdatedAt *datatype.DateTime
CustomClaims []CustomClaim
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
@@ -87,18 +85,10 @@ func (u User) Initials() string {
return strings.ToUpper(first + last)
}
func (u User) LastModified() time.Time {
if u.UpdatedAt != nil {
return u.UpdatedAt.ToTime()
}
return u.CreatedAt.ToTime()
}
type OneTimeAccessToken struct {
Base
Token string
DeviceToken *string
ExpiresAt datatype.DateTime
Token string
ExpiresAt datatype.DateTime
UserID string
User User

View File

@@ -1,25 +1,10 @@
package model
import (
"time"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
)
type UserGroup struct {
Base
FriendlyName string `sortable:"true"`
Name string `sortable:"true"`
LdapID *string
UpdatedAt *datatype.DateTime
Users []User `gorm:"many2many:user_groups_users;"`
CustomClaims []CustomClaim
AllowedOidcClients []OidcClient `gorm:"many2many:oidc_clients_allowed_user_groups;"`
}
func (ug UserGroup) LastModified() time.Time {
if ug.UpdatedAt != nil {
return ug.UpdatedAt.ToTime()
}
return ug.CreatedAt.ToTime()
FriendlyName string `sortable:"true"`
Name string `sortable:"true"`
LdapID *string
Users []User `gorm:"many2many:user_groups_users;"`
CustomClaims []CustomClaim
}

View File

@@ -98,17 +98,6 @@ func (s *TestService) SeedDatabase(baseURL string) error {
DisplayName: "Craig Federighi",
IsAdmin: false,
},
{
Base: model.Base{
ID: "d9256384-98ad-49a7-bc58-99ad0b4dc23c",
},
Username: "eddy",
Email: utils.Ptr("eddy.cue@test.com"),
FirstName: "Eddy",
LastName: "Cue",
DisplayName: "Eddy Cue",
IsAdmin: false,
},
}
for _, user := range users {
if err := tx.Create(&user).Error; err != nil {
@@ -180,11 +169,10 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Base: model.Base{
ID: "606c7782-f2b1-49e5-8ea9-26eb1b06d018",
},
Name: "Immich",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
CreatedByID: utils.Ptr(users[1].ID),
IsGroupRestricted: true,
Name: "Immich",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
CreatedByID: utils.Ptr(users[1].ID),
AllowedUserGroups: []model.UserGroup{
userGroups[1],
},
@@ -197,7 +185,6 @@ 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"},
IsGroupRestricted: true,
CreatedByID: utils.Ptr(users[0].ID),
},
{
@@ -220,20 +207,6 @@ func (s *TestService) SeedDatabase(baseURL string) error {
},
},
},
{
Base: model.Base{
ID: "c46d2090-37a0-4f2b-8748-6aa53b0c1afa",
},
Name: "SCIM Client",
Secret: "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK", // nQbiuMRG7FpdK2EnDd5MBivWQeKFXohn
CallbackURLs: model.UrlList{"http://scimclient/auth/callback"},
CreatedByID: utils.Ptr(users[0].ID),
IsGroupRestricted: true,
AllowedUserGroups: []model.UserGroup{
userGroups[0],
userGroups[1],
},
},
}
for _, client := range oidcClients {
if err := tx.Create(&client).Error; err != nil {
@@ -376,9 +349,6 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
UsageLimit: 1,
UsageCount: 0,
UserGroups: []model.UserGroup{
userGroups[0],
},
},
{
Base: model.Base{

View File

@@ -78,7 +78,7 @@ func SendEmail[V any](ctx context.Context, srv *EmailService, toEmail email.Addr
data := &email.TemplateData[V]{
AppName: dbConfig.AppName.Value,
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
LogoURL: common.EnvConfig.AppURL + "/api/application-images/logo",
Data: tData,
}

View File

@@ -56,7 +56,7 @@ func (s *ExportService) extractDatabase() (DatabaseExport, error) {
Tables: map[string][]map[string]any{},
// These tables need to be inserted in a specific order because of foreign key constraints
// Not all tables are listed here, because not all tables are order-dependent
TableOrder: []string{"users", "user_groups", "oidc_clients", "signup_tokens"},
TableOrder: []string{"users", "user_groups", "oidc_clients"},
}
for table := range schema {

View File

@@ -61,7 +61,7 @@ func (s *ImportService) ImportFromZip(ctx context.Context, r *zip.Reader) error
// ImportDatabase only imports the database data from the given DatabaseExport struct.
func (s *ImportService) ImportDatabase(dbData DatabaseExport) error {
err := s.resetSchema(dbData.Version)
err := s.resetSchema(dbData.Version, dbData.Provider)
if err != nil {
return err
}
@@ -144,7 +144,7 @@ func (s *ImportService) importUploads(ctx context.Context, files []*zip.File) er
}
// resetSchema drops the existing schema and migrates to the target version
func (s *ImportService) resetSchema(targetVersion uint) error {
func (s *ImportService) resetSchema(targetVersion uint, exportDbProvider string) error {
sqlDb, err := s.db.DB()
if err != nil {
return fmt.Errorf("failed to get sql.DB: %w", err)
@@ -155,19 +155,11 @@ func (s *ImportService) resetSchema(targetVersion uint) error {
return fmt.Errorf("failed to get migrate instance: %w", err)
}
if s.db.Name() == "sqlite" {
s.db.Exec("PRAGMA foreign_keys = OFF;")
}
err = m.Drop()
if err != nil {
return fmt.Errorf("failed to drop existing schema: %w", err)
}
if s.db.Name() == "sqlite" {
defer s.db.Exec("PRAGMA foreign_keys = ON;")
}
// Needs to be called again to re-create the schema_migrations table
m, err = utils.GetEmbeddedMigrateInstance(sqlDb)
if err != nil {

View File

@@ -15,6 +15,7 @@ import (
"net/http"
"net/url"
"path"
"regexp"
"slices"
"strings"
"time"
@@ -168,7 +169,7 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
return "", "", err
}
if !IsUserGroupAllowedToAuthorize(user, client) {
if !s.IsUserGroupAllowedToAuthorize(user, client) {
return "", "", &common.OidcAccessDeniedError{}
}
@@ -224,8 +225,8 @@ func (s *OidcService) hasAuthorizedClientInternal(ctx context.Context, clientID,
}
// IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client
func IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
if !client.IsGroupRestricted {
func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
if len(client.AllowedUserGroups) == 0 {
return true
}
@@ -777,14 +778,6 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
updateOIDCClientModelFromDto(&client, &input)
if !input.IsGroupRestricted {
// Clear allowed user groups if the restriction is removed
err = tx.Model(&client).Association("AllowedUserGroups").Clear()
if err != nil {
return model.OidcClient{}, err
}
}
err = tx.WithContext(ctx).Save(&client).Error
if err != nil {
return model.OidcClient{}, err
@@ -823,7 +816,6 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
client.PkceEnabled = input.IsPublic || input.PkceEnabled
client.RequiresReauthentication = input.RequiresReauthentication
client.LaunchURL = input.LaunchURL
client.IsGroupRestricted = input.IsGroupRestricted
// Credentials
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
@@ -1204,7 +1196,7 @@ func (s *OidcService) getCallbackURL(client *model.OidcClient, inputCallbackURL
// If URLs are already configured, validate against them
if len(client.CallbackURLs) > 0 {
matched, err := utils.GetCallbackURLFromList(client.CallbackURLs, inputCallbackURL)
matched, err := s.getCallbackURLFromList(client.CallbackURLs, inputCallbackURL)
if err != nil {
return "", err
} else if matched == "" {
@@ -1227,7 +1219,7 @@ func (s *OidcService) getLogoutCallbackURL(client *model.OidcClient, inputLogout
return client.LogoutCallbackURLs[0], nil
}
matched, err := utils.GetCallbackURLFromList(client.LogoutCallbackURLs, inputLogoutCallbackURL)
matched, err := s.getCallbackURLFromList(client.LogoutCallbackURLs, inputLogoutCallbackURL)
if err != nil {
return "", err
} else if matched == "" {
@@ -1237,6 +1229,21 @@ func (s *OidcService) getLogoutCallbackURL(client *model.OidcClient, inputLogout
return matched, nil
}
func (s *OidcService) getCallbackURLFromList(urls []string, inputCallbackURL string) (callbackURL string, err error) {
for _, callbackPattern := range urls {
regexPattern := "^" + strings.ReplaceAll(regexp.QuoteMeta(callbackPattern), `\*`, ".*") + "$"
matched, err := regexp.MatchString(regexPattern, inputCallbackURL)
if err != nil {
return "", err
}
if matched {
return inputCallbackURL, nil
}
}
return "", nil
}
func (s *OidcService) addCallbackURLToClient(ctx context.Context, client *model.OidcClient, callbackURL string, tx *gorm.DB) error {
// Add the new callback URL to the existing list
client.CallbackURLs = append(client.CallbackURLs, callbackURL)
@@ -1325,7 +1332,7 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
return fmt.Errorf("error finding user groups: %w", err)
}
if !IsUserGroupAllowedToAuthorize(user, deviceAuth.Client) {
if !s.IsUserGroupAllowedToAuthorize(user, deviceAuth.Client) {
return &common.OidcAccessDeniedError{}
}
@@ -1426,7 +1433,6 @@ func (s *OidcService) GetAllowedGroupsCountOfClient(ctx context.Context, id stri
}
func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {
query := s.db.
WithContext(ctx).
Model(&model.UserAuthorizedOidcClient{}).
@@ -1829,7 +1835,7 @@ func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, use
return nil, err
}
if !IsUserGroupAllowedToAuthorize(user, client) {
if !s.IsUserGroupAllowedToAuthorize(user, client) {
return nil, &common.OidcAccessDeniedError{}
}
@@ -1956,7 +1962,7 @@ func (s *OidcService) IsClientAccessibleToUser(ctx context.Context, clientID str
return false, err
}
return IsUserGroupAllowedToAuthorize(user, client), nil
return s.IsUserGroupAllowedToAuthorize(user, client), nil
}
var errLogoTooLarge = errors.New("logo is too large")
@@ -2117,15 +2123,105 @@ func (s *OidcService) updateClientLogoType(ctx context.Context, clientID string,
return nil
}
func (s *OidcService) GetClientScimServiceProvider(ctx context.Context, clientID string) (model.ScimServiceProvider, error) {
var provider model.ScimServiceProvider
err := s.db.
WithContext(ctx).
First(&provider, "oidc_client_id = ?", clientID).
Error
if err != nil {
return model.ScimServiceProvider{}, err
func (s *OidcService) CreateAPI(ctx context.Context, input dto.OidcAPICreateDto) (model.OidcAPI, error) {
api := model.OidcAPI{
Name: input.Name,
}
return provider, nil
err := s.db.
WithContext(ctx).
Create(&api).
Error
if err != nil {
return model.OidcAPI{}, fmt.Errorf("failed to create API in database: %w", err)
}
return api, nil
}
func (s *OidcService) GetAPI(ctx context.Context, id string) (model.OidcAPI, error) {
var api model.OidcAPI
err := s.db.
WithContext(ctx).
First(&api, "id = ?", id).
Error
if err != nil {
return model.OidcAPI{}, fmt.Errorf("failed to get API from database: %w", err)
}
return api, nil
}
func (s *OidcService) ListAPIs(ctx context.Context, searchTerm string, listRequestOptions utils.ListRequestOptions) ([]model.OidcAPI, utils.PaginationResponse, error) {
var apis []model.OidcAPI
query := s.db.
WithContext(ctx).
Model(&model.OidcAPI{})
if searchTerm != "" {
query = query.Where("id = ? OR name = ? OR identifier = ?", searchTerm, searchTerm, searchTerm)
}
response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &apis)
return apis, response, err
}
func (s *OidcService) UpdateAPI(ctx context.Context, id string, input dto.OidcAPIUpdateDto) (model.OidcAPI, error) {
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
var api model.OidcAPI
err := tx.
WithContext(ctx).
First(&api, "id = ?", id).
Error
if err != nil {
return model.OidcAPI{}, fmt.Errorf("failed to get API from database: %w", err)
}
api.Name = input.Name
api.Identifier = input.Identifier
// Convert permissions from DTO to model
api.Data.Permissions = make([]model.OidcAPIPermission, len(input.Permissions))
for i, p := range input.Permissions {
api.Data.Permissions[i] = model.OidcAPIPermission{
Name: p.Name,
Description: p.Description,
}
}
err = tx.
WithContext(ctx).
Save(&api).
Error
if err != nil {
return model.OidcAPI{}, fmt.Errorf("failed to save API in database: %w", err)
}
err = tx.Commit().Error
if err != nil {
return model.OidcAPI{}, fmt.Errorf("failed to commit transaction: %w", err)
}
return api, nil
}
func (s *OidcService) DeleteAPI(ctx context.Context, id string) error {
result := s.db.
WithContext(ctx).
Delete(&model.OidcAPI{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@@ -1,136 +0,0 @@
package service
import (
"context"
"errors"
"log/slog"
"sync"
"time"
"gorm.io/gorm"
)
// ScimSchedulerService schedules and triggers periodic synchronization
// of SCIM service providers. Each provider is tracked independently,
// and sync operations are run at or after their scheduled time.
type ScimSchedulerService struct {
scimService *ScimService
providerSyncTime map[string]time.Time
mu sync.RWMutex
}
func NewScimSchedulerService(ctx context.Context, scimService *ScimService) (*ScimSchedulerService, error) {
s := &ScimSchedulerService{
scimService: scimService,
providerSyncTime: make(map[string]time.Time),
}
err := s.start(ctx)
return s, err
}
// ScheduleSync forces the given provider to be synced soon by
// moving its next scheduled time to 5 minutes from now.
func (s *ScimSchedulerService) ScheduleSync(providerID string) {
s.setSyncTime(providerID, 5*time.Minute)
}
// start initializes the scheduler and begins the synchronization loop.
// Syncs happen every hour by default, but ScheduleSync can be called to schedule a sync sooner.
func (s *ScimSchedulerService) start(ctx context.Context) error {
if err := s.refreshProviders(ctx); err != nil {
return err
}
go func() {
const (
syncCheckInterval = 5 * time.Second
providerRefreshDelay = time.Minute
)
ticker := time.NewTicker(syncCheckInterval)
defer ticker.Stop()
lastProviderRefresh := time.Now()
for {
select {
case <-ctx.Done():
return
// Runs every 5 seconds to check if any provider is due for sync
case <-ticker.C:
now := time.Now()
if now.Sub(lastProviderRefresh) >= providerRefreshDelay {
err := s.refreshProviders(ctx)
if err != nil {
slog.Error("Error refreshing SCIM service providers",
slog.Any("error", err),
)
} else {
lastProviderRefresh = now
}
}
var due []string
s.mu.RLock()
for providerID, syncTime := range s.providerSyncTime {
if !syncTime.After(now) {
due = append(due, providerID)
}
}
s.mu.RUnlock()
s.syncProviders(ctx, due)
}
}
}()
return nil
}
func (s *ScimSchedulerService) refreshProviders(ctx context.Context) error {
providers, err := s.scimService.ListServiceProviders(ctx)
if err != nil {
return err
}
inAHour := time.Now().Add(time.Hour)
s.mu.Lock()
for _, provider := range providers {
if _, exists := s.providerSyncTime[provider.ID]; !exists {
s.providerSyncTime[provider.ID] = inAHour
}
}
s.mu.Unlock()
return nil
}
func (s *ScimSchedulerService) syncProviders(ctx context.Context, providerIDs []string) {
for _, providerID := range providerIDs {
err := s.scimService.SyncServiceProvider(ctx, providerID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Remove the provider from the schedule if it no longer exists
s.mu.Lock()
delete(s.providerSyncTime, providerID)
s.mu.Unlock()
} else {
slog.Error("Error syncing SCIM client",
slog.String("provider_id", providerID),
slog.Any("error", err),
)
}
continue
}
// A successful sync schedules the next sync in an hour
s.setSyncTime(providerID, time.Hour)
}
}
func (s *ScimSchedulerService) setSyncTime(providerID string, t time.Duration) {
s.mu.Lock()
s.providerSyncTime[providerID] = time.Now().Add(t)
s.mu.Unlock()
}

View File

@@ -1,774 +0,0 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"gorm.io/gorm"
)
const (
scimUserSchema = "urn:ietf:params:scim:schemas:core:2.0:User"
scimGroupSchema = "urn:ietf:params:scim:schemas:core:2.0:Group"
scimContentType = "application/scim+json"
)
const scimErrorBodyLimit = 4096
type scimSyncAction int
const (
scimActionNone scimSyncAction = iota
scimActionCreated
scimActionUpdated
scimActionDeleted
)
type scimSyncStats struct {
Created int
Updated int
Deleted int
}
// ScimService handles SCIM provisioning to external service providers.
type ScimService struct {
db *gorm.DB
httpClient *http.Client
}
func NewScimService(db *gorm.DB, httpClient *http.Client) *ScimService {
if httpClient == nil {
httpClient = &http.Client{Timeout: 20 * time.Second}
}
return &ScimService{db: db, httpClient: httpClient}
}
func (s *ScimService) GetServiceProvider(
ctx context.Context,
serviceProviderID string,
) (model.ScimServiceProvider, error) {
var provider model.ScimServiceProvider
err := s.db.WithContext(ctx).
Preload("OidcClient").
Preload("OidcClient.AllowedUserGroups").
First(&provider, "id = ?", serviceProviderID).
Error
if err != nil {
return model.ScimServiceProvider{}, err
}
return provider, nil
}
func (s *ScimService) ListServiceProviders(ctx context.Context) ([]model.ScimServiceProvider, error) {
var providers []model.ScimServiceProvider
err := s.db.WithContext(ctx).
Preload("OidcClient").
Find(&providers).
Error
if err != nil {
return nil, err
}
return providers, nil
}
func (s *ScimService) CreateServiceProvider(
ctx context.Context,
input *dto.ScimServiceProviderCreateDTO) (model.ScimServiceProvider, error) {
provider := model.ScimServiceProvider{
Endpoint: input.Endpoint,
Token: datatype.EncryptedString(input.Token),
OidcClientID: input.OidcClientID,
}
if err := s.db.WithContext(ctx).Create(&provider).Error; err != nil {
return model.ScimServiceProvider{}, err
}
return provider, nil
}
func (s *ScimService) UpdateServiceProvider(ctx context.Context,
serviceProviderID string,
input *dto.ScimServiceProviderCreateDTO,
) (model.ScimServiceProvider, error) {
var provider model.ScimServiceProvider
err := s.db.WithContext(ctx).
First(&provider, "id = ?", serviceProviderID).
Error
if err != nil {
return model.ScimServiceProvider{}, err
}
provider.Endpoint = input.Endpoint
provider.Token = datatype.EncryptedString(input.Token)
provider.OidcClientID = input.OidcClientID
if err := s.db.WithContext(ctx).Save(&provider).Error; err != nil {
return model.ScimServiceProvider{}, err
}
return provider, nil
}
func (s *ScimService) DeleteServiceProvider(ctx context.Context, serviceProviderID string) error {
return s.db.WithContext(ctx).
Delete(&model.ScimServiceProvider{}, "id = ?", serviceProviderID).
Error
}
func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID string) error {
start := time.Now()
provider, err := s.GetServiceProvider(ctx, serviceProviderID)
if err != nil {
return err
}
slog.InfoContext(ctx, "Syncing SCIM service provider",
slog.String("provider_id", provider.ID),
slog.String("oidc_client_id", provider.OidcClientID),
)
allowedGroupIDs := groupIDs(provider.OidcClient.AllowedUserGroups)
// Load users and groups that should be synced to the SCIM provider
groups, err := s.groupsForClient(ctx, provider.OidcClient, allowedGroupIDs)
if err != nil {
return err
}
users, err := s.usersForClient(ctx, provider.OidcClient, allowedGroupIDs)
if err != nil {
return err
}
// Load users and groups that already exist in the SCIM provider
userResources, err := listScimResources[dto.ScimUser](s, ctx, provider, "/Users")
if err != nil {
return err
}
groupResources, err := listScimResources[dto.ScimGroup](s, ctx, provider, "/Groups")
if err != nil {
return err
}
var errs []error
var userStats scimSyncStats
var groupStats scimSyncStats
// Sync users first, so that groups can reference them
if stats, err := s.syncUsers(ctx, provider, users, &userResources); err != nil {
errs = append(errs, err)
userStats = stats
} else {
userStats = stats
}
stats, err := s.syncGroups(ctx, provider, groups, groupResources.Resources, userResources.Resources)
if err != nil {
errs = append(errs, err)
groupStats = stats
} else {
groupStats = stats
}
if len(errs) > 0 {
slog.WarnContext(ctx, "SCIM sync completed with errors",
slog.String("provider_id", provider.ID),
slog.Int("error_count", len(errs)),
slog.Int("users_created", userStats.Created),
slog.Int("users_updated", userStats.Updated),
slog.Int("users_deleted", userStats.Deleted),
slog.Int("groups_created", groupStats.Created),
slog.Int("groups_updated", groupStats.Updated),
slog.Int("groups_deleted", groupStats.Deleted),
slog.Duration("duration", time.Since(start)),
)
return errors.Join(errs...)
}
provider.LastSyncedAt = utils.Ptr(datatype.DateTime(time.Now()))
if err := s.db.WithContext(ctx).Save(&provider).Error; err != nil {
return err
}
slog.InfoContext(ctx, "SCIM sync completed",
slog.String("provider_id", provider.ID),
slog.Int("users_created", userStats.Created),
slog.Int("users_updated", userStats.Updated),
slog.Int("users_deleted", userStats.Deleted),
slog.Int("groups_created", groupStats.Created),
slog.Int("groups_updated", groupStats.Updated),
slog.Int("groups_deleted", groupStats.Deleted),
slog.Duration("duration", time.Since(start)),
)
return nil
}
func (s *ScimService) syncUsers(
ctx context.Context,
provider model.ScimServiceProvider,
users []model.User,
resourceList *dto.ScimListResponse[dto.ScimUser],
) (stats scimSyncStats, err error) {
var errs []error
// Update or create users
for _, u := range users {
existing := getResourceByExternalID[dto.ScimUser](u.ID, resourceList.Resources)
action, created, err := s.syncUser(ctx, provider, u, existing)
if created != nil && existing == nil {
resourceList.Resources = append(resourceList.Resources, *created)
}
if err != nil {
errs = append(errs, err)
continue
}
// Update stats based on action taken by syncUser
switch action {
case scimActionCreated:
stats.Created++
case scimActionUpdated:
stats.Updated++
case scimActionDeleted:
stats.Deleted++
case scimActionNone:
}
}
// Delete users that are present in SCIM provider but not locally.
userSet := make(map[string]struct{})
for _, u := range users {
userSet[u.ID] = struct{}{}
}
for _, r := range resourceList.Resources {
if _, ok := userSet[r.ExternalID]; !ok {
if err := s.deleteScimResource(ctx, provider, "/Users/"+url.PathEscape(r.ID)); err != nil {
errs = append(errs, err)
} else {
stats.Deleted++
}
}
}
return stats, errors.Join(errs...)
}
func (s *ScimService) syncGroups(
ctx context.Context,
provider model.ScimServiceProvider,
groups []model.UserGroup,
remoteGroups []dto.ScimGroup,
userResources []dto.ScimUser,
) (stats scimSyncStats, err error) {
var errs []error
// Update or create groups
for _, g := range groups {
existing := getResourceByExternalID[dto.ScimGroup](g.ID, remoteGroups)
action, err := s.syncGroup(ctx, provider, g, existing, userResources)
if err != nil {
errs = append(errs, err)
continue
}
// Update stats based on action taken by syncGroup
switch action {
case scimActionCreated:
stats.Created++
case scimActionUpdated:
stats.Updated++
case scimActionDeleted:
stats.Deleted++
case scimActionNone:
}
}
// Delete groups that are present in SCIM provider but not locally
groupSet := make(map[string]struct{})
for _, g := range groups {
groupSet[g.ID] = struct{}{}
}
for _, r := range remoteGroups {
if _, ok := groupSet[r.ExternalID]; !ok {
if err := s.deleteScimResource(ctx, provider, "/Groups/"+url.PathEscape(r.GetID())); err != nil {
errs = append(errs, err)
} else {
stats.Deleted++
}
}
}
return stats, errors.Join(errs...)
}
func (s *ScimService) syncUser(ctx context.Context,
provider model.ScimServiceProvider,
user model.User,
userResource *dto.ScimUser,
) (scimSyncAction, *dto.ScimUser, error) {
// If user is not allowed for the client, delete it from SCIM provider
if userResource != nil && !IsUserGroupAllowedToAuthorize(user, provider.OidcClient) {
return scimActionDeleted, nil, s.deleteScimResource(ctx, provider, fmt.Sprintf("/Users/%s", url.PathEscape(userResource.ID)))
}
payload := dto.ScimUser{
ScimResourceData: dto.ScimResourceData{
Schemas: []string{scimUserSchema},
ExternalID: user.ID,
},
UserName: user.Username,
Name: &dto.ScimName{
GivenName: user.FirstName,
FamilyName: user.LastName,
},
Display: user.DisplayName,
Active: !user.Disabled,
}
if user.Email != nil {
payload.Emails = []dto.ScimEmail{{
Value: *user.Email,
Primary: true,
}}
}
// If the user exists on the SCIM provider, and it has been modified, update it
if userResource != nil {
if user.LastModified().Before(userResource.GetMeta().LastModified) {
return scimActionNone, nil, nil
}
path := fmt.Sprintf("/Users/%s", url.PathEscape(userResource.GetID()))
userResource, err := updateScimResource(s, ctx, provider, path, payload)
if err != nil {
return scimActionNone, nil, err
}
return scimActionUpdated, userResource, nil
}
// Otherwise, create a new SCIM user
userResource, err := createScimResource(s, ctx, provider, "/Users", payload)
if err != nil {
return scimActionNone, nil, err
}
return scimActionCreated, userResource, nil
}
func (s *ScimService) syncGroup(
ctx context.Context,
provider model.ScimServiceProvider,
group model.UserGroup,
groupResource *dto.ScimGroup,
userResources []dto.ScimUser,
) (scimSyncAction, error) {
// If group is not allowed for the client, delete it from SCIM provider
if groupResource != nil && !groupAllowedForClient(group.ID, provider.OidcClient) {
return scimActionDeleted, s.deleteScimResource(ctx, provider, fmt.Sprintf("/Groups/%s", url.PathEscape(groupResource.GetID())))
}
// Prepare group members
members := make([]dto.ScimGroupMember, len(group.Users))
for i, user := range group.Users {
userResource := getResourceByExternalID[dto.ScimUser](user.ID, userResources)
if userResource == nil {
// Groups depend on user IDs already being provisioned
return scimActionNone, fmt.Errorf("cannot sync group %s: user %s is not provisioned in SCIM provider", group.ID, user.ID)
}
members[i] = dto.ScimGroupMember{
Value: userResource.GetID(),
}
}
groupPayload := dto.ScimGroup{
ScimResourceData: dto.ScimResourceData{
Schemas: []string{scimGroupSchema},
ExternalID: group.ID,
},
Display: group.FriendlyName,
Members: members,
}
// If the group exists on the SCIM provider, and it has been modified, update it
if groupResource != nil {
if group.LastModified().Before(groupResource.GetMeta().LastModified) {
return scimActionNone, nil
}
path := fmt.Sprintf("/Groups/%s", url.PathEscape(groupResource.GetID()))
_, err := updateScimResource(s, ctx, provider, path, groupPayload)
if err != nil {
return scimActionNone, err
}
return scimActionUpdated, nil
}
// Otherwise, create a new SCIM group
_, err := createScimResource(s, ctx, provider, "/Groups", groupPayload)
if err != nil {
return scimActionNone, err
}
return scimActionCreated, nil
}
func groupAllowedForClient(groupID string, client model.OidcClient) bool {
if !client.IsGroupRestricted {
return true
}
for _, allowedGroup := range client.AllowedUserGroups {
if allowedGroup.ID == groupID {
return true
}
}
return false
}
func groupIDs(groups []model.UserGroup) []string {
ids := make([]string, len(groups))
for i, g := range groups {
ids[i] = g.ID
}
return ids
}
func (s *ScimService) groupsForClient(
ctx context.Context,
client model.OidcClient,
allowedGroupIDs []string,
) ([]model.UserGroup, error) {
var groups []model.UserGroup
query := s.db.WithContext(ctx).Preload("Users").Model(&model.UserGroup{})
if client.IsGroupRestricted {
if len(allowedGroupIDs) == 0 {
return groups, nil
}
query = query.Where("id IN ?", allowedGroupIDs)
}
if err := query.Find(&groups).Error; err != nil {
return nil, err
}
return groups, nil
}
func (s *ScimService) usersForClient(
ctx context.Context,
client model.OidcClient,
allowedGroupIDs []string,
) ([]model.User, error) {
var users []model.User
query := s.db.WithContext(ctx).Model(&model.User{})
if client.IsGroupRestricted {
if len(allowedGroupIDs) == 0 {
return users, nil
}
query = query.
Joins("JOIN user_groups_users ON users.id = user_groups_users.user_id").
Where("user_groups_users.user_group_id IN ?", allowedGroupIDs).
Select("users.*").
Distinct()
}
query = query.Preload("UserGroups")
if err := query.Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
func getResourceByExternalID[T dto.ScimResource](externalID string, resource []T) *T {
for i := range resource {
if resource[i].GetExternalID() == externalID {
return &resource[i]
}
}
return nil
}
func listScimResources[T any](
s *ScimService,
ctx context.Context,
provider model.ScimServiceProvider,
path string,
) (result dto.ScimListResponse[T], err error) {
startIndex := 1
count := 1000
for {
// Use SCIM pagination to avoid missing resources on large providers
queryParams := map[string]string{
"startIndex": strconv.Itoa(startIndex),
"count": strconv.Itoa(count),
}
resp, err := s.scimRequest(ctx, provider, http.MethodGet, path, nil, queryParams)
if err != nil {
return dto.ScimListResponse[T]{}, err
}
if err := ensureScimStatus(ctx, resp, provider, http.StatusOK); err != nil {
return dto.ScimListResponse[T]{}, err
}
var page dto.ScimListResponse[T]
if err := json.NewDecoder(resp.Body).Decode(&page); err != nil {
return dto.ScimListResponse[T]{}, fmt.Errorf("failed to decode SCIM list response: %w", err)
}
resp.Body.Close()
// Initialize metadata only once
if result.TotalResults == 0 {
result.TotalResults = page.TotalResults
}
result.Resources = append(result.Resources, page.Resources...)
// If we've fetched everything, stop
if len(result.Resources) >= page.TotalResults || len(page.Resources) == 0 {
break
}
startIndex += page.ItemsPerPage
}
result.ItemsPerPage = len(result.Resources)
return result, nil
}
func createScimResource[T dto.ScimResource](
s *ScimService,
ctx context.Context,
provider model.ScimServiceProvider,
path string, payload T) (*T, error) {
resp, err := s.scimRequest(ctx, provider, http.MethodPost, path, payload, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := ensureScimStatus(ctx, resp, provider, http.StatusOK, http.StatusCreated); err != nil {
return nil, err
}
var resource T
if err := json.NewDecoder(resp.Body).Decode(&resource); err != nil {
return nil, fmt.Errorf("failed to decode SCIM create response: %w", err)
}
return &resource, nil
}
func updateScimResource[T dto.ScimResource](
s *ScimService,
ctx context.Context,
provider model.ScimServiceProvider,
path string,
payload T,
) (*T, error) {
resp, err := s.scimRequest(ctx, provider, http.MethodPut, path, payload, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := ensureScimStatus(ctx, resp, provider, http.StatusOK, http.StatusCreated); err != nil {
return nil, err
}
var resource T
if err := json.NewDecoder(resp.Body).Decode(&resource); err != nil {
return nil, fmt.Errorf("failed to decode SCIM update response: %w", err)
}
return &resource, nil
}
func (s *ScimService) deleteScimResource(ctx context.Context, provider model.ScimServiceProvider, path string) error {
resp, err := s.scimRequest(ctx, provider, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil
}
return ensureScimStatus(ctx, resp, provider, http.StatusOK, http.StatusNoContent)
}
func (s *ScimService) scimRequest(
ctx context.Context,
provider model.ScimServiceProvider,
method,
path string,
payload any,
queryParams map[string]string,
) (*http.Response, error) {
urlString, err := scimURL(provider.Endpoint, path, queryParams)
if err != nil {
return nil, err
}
var bodyBytes []byte
if payload != nil {
encoded, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to encode SCIM payload: %w", err)
}
bodyBytes = encoded
}
retryAttempts := 3
for attempt := 1; attempt <= retryAttempts; attempt++ {
var body io.Reader
if bodyBytes != nil {
body = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequestWithContext(ctx, method, urlString, body)
if err != nil {
return nil, err
}
req.Header.Set("Accept", scimContentType)
if payload != nil {
req.Header.Set("Content-Type", scimContentType)
}
token := string(provider.Token)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
slog.Debug("Sending SCIM request",
slog.String("method", method),
slog.String("url", urlString),
slog.String("provider_id", provider.ID),
)
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}
// Only retry on 429 to avoid masking other errors
if resp.StatusCode != http.StatusTooManyRequests || attempt == retryAttempts {
return resp, nil
}
retryDelay := scimRetryDelay(resp.Header.Get("Retry-After"), attempt)
slog.WarnContext(ctx, "SCIM provider rate-limited, retrying",
slog.String("provider_id", provider.ID),
slog.String("method", method),
slog.String("url", urlString),
slog.Int("attempt", attempt),
slog.Duration("retry_after", retryDelay),
)
resp.Body.Close()
if err := utils.SleepWithContext(ctx, retryDelay); err != nil {
return nil, err
}
}
return nil, fmt.Errorf("scim request retry attempts exceeded")
}
func scimRetryDelay(retryAfter string, attempt int) time.Duration {
// Respect Retry-After when provided
if retryAfter != "" {
if seconds, err := strconv.Atoi(retryAfter); err == nil {
return time.Duration(seconds) * time.Second
}
if t, err := http.ParseTime(retryAfter); err == nil {
if delay := time.Until(t); delay > 0 {
return delay
}
}
}
// Exponential backoff otherwise
maxDelay := 10 * time.Second
delay := 500 * time.Millisecond * (time.Duration(1) << (attempt - 1)) //nolint:gosec // attempt is bounded 1-3
if delay > maxDelay {
return maxDelay
}
return delay
}
func scimURL(endpoint, p string, queryParams map[string]string) (string, error) {
u, err := url.Parse(endpoint)
if err != nil {
return "", fmt.Errorf("invalid scim endpoint: %w", err)
}
u.Path = path.Join(strings.TrimRight(u.Path, "/"), p)
q := u.Query()
for key, value := range queryParams {
q.Set(key, value)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
func ensureScimStatus(
ctx context.Context,
resp *http.Response,
provider model.ScimServiceProvider,
allowedStatuses ...int) error {
for _, status := range allowedStatuses {
if resp.StatusCode == status {
return nil
}
}
body := readScimErrorBody(resp.Body)
slog.ErrorContext(ctx, "SCIM request failed",
slog.String("provider_id", provider.ID),
slog.String("method", resp.Request.Method),
slog.String("url", resp.Request.URL.String()),
slog.Int("status", resp.StatusCode),
slog.String("response_body", body),
)
return fmt.Errorf("scim request failed with status %d: %s", resp.StatusCode, body)
}
func readScimErrorBody(body io.Reader) string {
payload, err := io.ReadAll(io.LimitReader(body, scimErrorBodyLimit))
if err != nil {
return ""
}
return strings.TrimSpace(string(payload))
}

View File

@@ -3,9 +3,7 @@ package service
import (
"context"
"errors"
"time"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
@@ -55,7 +53,6 @@ func (s *UserGroupService) getInternal(ctx context.Context, id string, tx *gorm.
Where("id = ?", id).
Preload("CustomClaims").
Preload("Users").
Preload("AllowedOidcClients").
First(&group).
Error
return group, err
@@ -153,7 +150,6 @@ func (s *UserGroupService) updateInternal(ctx context.Context, id string, input
group.Name = input.Name
group.FriendlyName = input.FriendlyName
group.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now()))
err = tx.
WithContext(ctx).
@@ -217,8 +213,6 @@ func (s *UserGroupService) updateUsersInternal(ctx context.Context, id string, u
}
// Save the updated group
group.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now()))
err = tx.
WithContext(ctx).
Save(&group).
@@ -254,54 +248,3 @@ func (s *UserGroupService) GetUserCountOfGroup(ctx context.Context, id string) (
Count()
return count, nil
}
func (s *UserGroupService) UpdateAllowedOidcClient(ctx context.Context, id string, input dto.UserGroupUpdateAllowedOidcClientsDto) (group model.UserGroup, err error) {
tx := s.db.Begin()
defer func() {
tx.Rollback()
}()
group, err = s.getInternal(ctx, id, tx)
if err != nil {
return model.UserGroup{}, err
}
// Fetch the clients based on the client IDs
var clients []model.OidcClient
if len(input.OidcClientIDs) > 0 {
err = tx.
WithContext(ctx).
Where("id IN (?)", input.OidcClientIDs).
Find(&clients).
Error
if err != nil {
return model.UserGroup{}, err
}
}
// Replace the current clients with the new set of clients
err = tx.
WithContext(ctx).
Model(&group).
Association("AllowedOidcClients").
Replace(clients)
if err != nil {
return model.UserGroup{}, err
}
// Save the updated group
err = tx.
WithContext(ctx).
Save(&group).
Error
if err != nil {
return model.UserGroup{}, err
}
err = tx.Commit().Error
if err != nil {
return model.UserGroup{}, err
}
return group, nil
}

View File

@@ -253,18 +253,6 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
return model.User{}, &common.UserEmailNotSetError{}
}
var userGroups []model.UserGroup
if len(input.UserGroupIds) > 0 {
err := tx.
WithContext(ctx).
Where("id IN ?", input.UserGroupIds).
Find(&userGroups).
Error
if err != nil {
return model.User{}, err
}
}
user := model.User{
FirstName: input.FirstName,
LastName: input.LastName,
@@ -274,7 +262,6 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
IsAdmin: input.IsAdmin,
Locale: input.Locale,
Disabled: input.Disabled,
UserGroups: userGroups,
}
if input.LdapID != "" {
user.LdapID = &input.LdapID
@@ -298,13 +285,7 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
// Apply default groups and claims for new non-LDAP users
if !isLdapSync {
if len(input.UserGroupIds) == 0 {
if err := s.applyDefaultGroups(ctx, &user, tx); err != nil {
return model.User{}, err
}
}
if err := s.applyDefaultCustomClaims(ctx, &user, tx); err != nil {
if err := s.applySignupDefaults(ctx, &user, tx); err != nil {
return model.User{}, err
}
}
@@ -312,9 +293,10 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
return user, nil
}
func (s *UserService) applyDefaultGroups(ctx context.Context, user *model.User, tx *gorm.DB) error {
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
v := config.SignupDefaultUserGroupIDs.Value
if v != "" && v != "[]" {
@@ -341,14 +323,10 @@ func (s *UserService) applyDefaultGroups(ctx context.Context, user *model.User,
}
}
}
return nil
}
func (s *UserService) applyDefaultCustomClaims(ctx context.Context, user *model.User, tx *gorm.DB) error {
config := s.appConfigService.GetDbConfig()
// Apply default custom claims
var claims []dto.CustomClaimCreateDto
v := config.SignupDefaultCustomClaims.Value
v = config.SignupDefaultCustomClaims.Value
if v != "" && v != "[]" {
err := json.Unmarshal([]byte(v), &claims)
if err != nil {
@@ -426,8 +404,6 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
}
}
user.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now()))
err = tx.
WithContext(ctx).
Save(&user).
@@ -456,36 +432,28 @@ func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, user
return &common.OneTimeAccessDisabledError{}
}
_, err := s.requestOneTimeAccessEmailInternal(ctx, userID, "", ttl, true)
return err
return s.requestOneTimeAccessEmailInternal(ctx, userID, "", ttl)
}
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) (string, error) {
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error {
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsUnauthenticatedEnabled.IsTrue()
if isDisabled {
return "", &common.OneTimeAccessDisabledError{}
return &common.OneTimeAccessDisabledError{}
}
var userId string
err := s.db.Model(&model.User{}).Select("id").Where("email = ?", userID).First(&userId).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// Do not return error if user not found to prevent email enumeration
return "", nil
return nil
} else if err != nil {
return "", err
return err
}
deviceToken, err := s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute, true)
if err != nil {
return "", err
} else if deviceToken == nil {
return "", errors.New("device token expected but not returned")
}
return *deviceToken, nil
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute)
}
func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, ttl time.Duration, withDeviceToken bool) (*string, error) {
func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, ttl time.Duration) error {
tx := s.db.Begin()
defer func() {
tx.Rollback()
@@ -493,20 +461,21 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
user, err := s.GetUser(ctx, userID)
if err != nil {
return nil, err
return err
}
if user.Email == nil {
return nil, &common.UserEmailNotSetError{}
return &common.UserEmailNotSetError{}
}
oneTimeAccessToken, deviceToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, withDeviceToken, tx)
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
if err != nil {
return nil, err
return err
}
err = tx.Commit().Error
if err != nil {
return nil, err
return err
}
// We use a background context here as this is running in a goroutine
@@ -539,29 +508,28 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
}
}()
return deviceToken, nil
return nil
}
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (token string, err error) {
token, _, err = s.createOneTimeAccessTokenInternal(ctx, userID, ttl, false, s.db)
return token, err
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, ttl time.Duration, withDeviceToken bool, tx *gorm.DB) (token string, deviceToken *string, err error) {
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, ttl, withDeviceToken)
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 "", nil, err
return "", err
}
err = tx.WithContext(ctx).Create(oneTimeAccessToken).Error
if err != nil {
return "", nil, err
return "", err
}
return oneTimeAccessToken.Token, oneTimeAccessToken.DeviceToken, nil
return oneTimeAccessToken.Token, nil
}
func (s *UserService) ExchangeOneTimeAccessToken(ctx context.Context, token, deviceToken, ipAddress, userAgent string) (model.User, string, error) {
func (s *UserService) ExchangeOneTimeAccessToken(ctx context.Context, token string, ipAddress, userAgent string) (model.User, string, error) {
tx := s.db.Begin()
defer func() {
tx.Rollback()
@@ -581,10 +549,6 @@ func (s *UserService) ExchangeOneTimeAccessToken(ctx context.Context, token, dev
}
return model.User{}, "", err
}
if oneTimeAccessToken.DeviceToken != nil && deviceToken != *oneTimeAccessToken.DeviceToken {
return model.User{}, "", &common.DeviceCodeInvalid{}
}
accessToken, err := s.jwtService.GenerateAccessToken(oneTimeAccessToken.User)
if err != nil {
return model.User{}, "", err
@@ -648,16 +612,6 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
return model.User{}, err
}
// Update the UpdatedAt field for all affected groups
now := time.Now()
for _, group := range groups {
group.UpdatedAt = utils.Ptr(datatype.DateTime(now))
err = tx.WithContext(ctx).Save(&group).Error
if err != nil {
return model.User{}, err
}
}
err = tx.Commit().Error
if err != nil {
return model.User{}, err
@@ -761,22 +715,12 @@ func (s *UserService) disableUserInternal(ctx context.Context, tx *gorm.DB, user
Error
}
func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int, userGroupIDs []string) (model.SignupToken, error) {
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
}
var userGroups []model.UserGroup
err = s.db.WithContext(ctx).
Where("id IN ?", userGroupIDs).
Find(&userGroups).
Error
if err != nil {
return model.SignupToken{}, err
}
signupToken.UserGroups = userGroups
err = s.db.WithContext(ctx).Create(signupToken).Error
if err != nil {
return model.SignupToken{}, err
@@ -799,11 +743,9 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
}
var signupToken model.SignupToken
var userGroupIDs []string
if tokenProvided {
err := tx.
WithContext(ctx).
Preload("UserGroups").
Where("token = ?", signupData.Token).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&signupToken).
@@ -818,19 +760,14 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
if !signupToken.IsValid() {
return model.User{}, "", &common.TokenInvalidOrExpiredError{}
}
for _, group := range signupToken.UserGroups {
userGroupIDs = append(userGroupIDs, group.ID)
}
}
userToCreate := dto.UserCreateDto{
Username: signupData.Username,
Email: signupData.Email,
FirstName: signupData.FirstName,
LastName: signupData.LastName,
DisplayName: strings.TrimSpace(signupData.FirstName + " " + signupData.LastName),
UserGroupIds: userGroupIDs,
Username: signupData.Username,
Email: signupData.Email,
FirstName: signupData.FirstName,
LastName: signupData.LastName,
DisplayName: strings.TrimSpace(signupData.FirstName + " " + signupData.LastName),
}
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
@@ -871,7 +808,7 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
func (s *UserService) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.SignupToken, utils.PaginationResponse, error) {
var tokens []model.SignupToken
query := s.db.WithContext(ctx).Preload("UserGroups").Model(&model.SignupToken{})
query := s.db.WithContext(ctx).Model(&model.SignupToken{})
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &tokens)
return tokens, pagination, err
@@ -881,33 +818,23 @@ 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, ttl time.Duration, withDeviceToken bool) (*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 ttl <= 15*time.Minute {
tokenLength = 6
}
token, err := utils.GenerateRandomUnambiguousString(tokenLength)
randomString, err := utils.GenerateRandomAlphanumericString(tokenLength)
if err != nil {
return nil, err
}
var deviceToken *string
if withDeviceToken {
dt, err := utils.GenerateRandomAlphanumericString(16)
if err != nil {
return nil, err
}
deviceToken = &dt
}
now := time.Now().Round(time.Second)
o := &model.OneTimeAccessToken{
UserID: userID,
ExpiresAt: datatype.DateTime(now.Add(ttl)),
Token: token,
DeviceToken: deviceToken,
UserID: userID,
ExpiresAt: datatype.DateTime(now.Add(ttl)),
Token: randomString,
}
return o, nil

View File

@@ -1,199 +0,0 @@
package utils
import (
"net"
"net/url"
"path"
"regexp"
"strings"
)
// GetCallbackURLFromList returns the first callback URL that matches the input callback URL
func GetCallbackURLFromList(urls []string, inputCallbackURL string) (callbackURL string, err error) {
// Special case for Loopback Interface Redirection. Quoting from RFC 8252 section 7.3:
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
//
// The authorization server MUST allow any port to be specified at the
// time of the request for loopback IP redirect URIs, to accommodate
// clients that obtain an available ephemeral port from the operating
// system at the time of the request.
loopbackRedirect := ""
u, _ := url.Parse(inputCallbackURL)
if u != nil && u.Scheme == "http" {
host := u.Hostname()
ip := net.ParseIP(host)
if host == "localhost" || (ip != nil && ip.IsLoopback()) {
loopbackRedirect = u.String()
u.Host = host
inputCallbackURL = u.String()
}
}
for _, pattern := range urls {
matches, err := matchCallbackURL(pattern, inputCallbackURL)
if err != nil {
return "", err
} else if !matches {
continue
}
if loopbackRedirect != "" {
return loopbackRedirect, nil
}
return inputCallbackURL, nil
}
return "", nil
}
// matchCallbackURL checks if the input callback URL matches the given pattern.
// It supports wildcard matching for paths and query parameters.
//
// The base URL (scheme, userinfo, host, port) and query parameters supports single '*' wildcards only,
// while the path supports both single '*' and double '**' wildcards.
func matchCallbackURL(pattern string, inputCallbackURL string) (matches bool, err error) {
if pattern == inputCallbackURL || pattern == "*" {
return true, nil
}
// Strip fragment part
// The endpoint URI MUST NOT include a fragment component.
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2
pattern, _, _ = strings.Cut(pattern, "#")
inputCallbackURL, _, _ = strings.Cut(inputCallbackURL, "#")
// Store and strip query part
var patternQuery url.Values
if i := strings.Index(pattern, "?"); i >= 0 {
patternQuery, err = url.ParseQuery(pattern[i+1:])
if err != nil {
return false, err
}
pattern = pattern[:i]
}
var inputQuery url.Values
if i := strings.Index(inputCallbackURL, "?"); i >= 0 {
inputQuery, err = url.ParseQuery(inputCallbackURL[i+1:])
if err != nil {
return false, err
}
inputCallbackURL = inputCallbackURL[:i]
}
// Split both pattern and input parts
patternParts, patternPath := splitParts(pattern)
inputParts, inputPath := splitParts(inputCallbackURL)
// Verify everything except the path and query parameters
if len(patternParts) != len(inputParts) {
return false, nil
}
for i, patternPart := range patternParts {
matched, err := path.Match(patternPart, inputParts[i])
if err != nil || !matched {
return false, err
}
}
// Verify path with wildcard support
matched, err := matchPath(patternPath, inputPath)
if err != nil || !matched {
return false, err
}
// Verify query parameters
if len(patternQuery) != len(inputQuery) {
return false, nil
}
for patternKey, patternValues := range patternQuery {
inputValues, exists := inputQuery[patternKey]
if !exists {
return false, nil
}
if len(patternValues) != len(inputValues) {
return false, nil
}
for i := range patternValues {
matched, err := path.Match(patternValues[i], inputValues[i])
if err != nil || !matched {
return false, err
}
}
}
return true, nil
}
// matchPath matches the input path against the pattern with wildcard support
// Supported wildcards:
//
// '*' matches any sequence of characters except '/'
// '**' matches any sequence of characters including '/'
func matchPath(pattern string, input string) (matches bool, err error) {
var regexPattern strings.Builder
regexPattern.WriteString("^")
runes := []rune(pattern)
n := len(runes)
for i := 0; i < n; {
switch runes[i] {
case '*':
// Check if it's a ** (globstar)
if i+1 < n && runes[i+1] == '*' {
// globstar = .* (match slashes too)
regexPattern.WriteString(".*")
i += 2
} else {
// single * = [^/]* (no slash)
regexPattern.WriteString(`[^/]*`)
i++
}
default:
regexPattern.WriteString(regexp.QuoteMeta(string(runes[i])))
i++
}
}
regexPattern.WriteString("$")
matched, err := regexp.MatchString(regexPattern.String(), input)
return matched, err
}
// splitParts splits the URL into parts by special characters and returns the path separately
func splitParts(s string) (parts []string, path string) {
split := func(r rune) bool {
return r == ':' || r == '/' || r == '[' || r == ']' || r == '@' || r == '.'
}
pathStart := -1
// Look for scheme:// first
if i := strings.Index(s, "://"); i >= 0 {
// Look for the next slash after scheme://
rest := s[i+3:]
if j := strings.IndexRune(rest, '/'); j >= 0 {
pathStart = i + 3 + j
}
} else {
// Otherwise, first slash is path start
pathStart = strings.IndexRune(s, '/')
}
if pathStart >= 0 {
path = s[pathStart:]
base := s[:pathStart]
parts = strings.FieldsFunc(base, split)
} else {
parts = strings.FieldsFunc(s, split)
path = ""
}
return parts, path
}

View File

@@ -1,784 +0,0 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMatchCallbackURL(t *testing.T) {
tests := []struct {
name string
pattern string
input string
shouldMatch bool
}{
// Basic matching
{
"exact match",
"https://example.com/callback",
"https://example.com/callback",
true,
},
{
"no match",
"https://example.org/callback",
"https://example.com/callback",
false,
},
// Scheme
{
"scheme mismatch",
"https://example.com/callback",
"http://example.com/callback",
false,
},
{
"wildcard scheme",
"*://example.com/callback",
"https://example.com/callback",
true,
},
// Hostname
{
"hostname mismatch",
"https://example.com/callback",
"https://malicious.com/callback",
false,
},
{
"wildcard subdomain",
"https://*.example.com/callback",
"https://subdomain.example.com/callback",
true,
},
{
"partial wildcard in hostname prefix",
"https://app*.example.com/callback",
"https://app1.example.com/callback",
true,
},
{
"partial wildcard in hostname suffix",
"https://*-prod.example.com/callback",
"https://api-prod.example.com/callback",
true,
},
{
"partial wildcard in hostname middle",
"https://app-*-server.example.com/callback",
"https://app-staging-server.example.com/callback",
true,
},
{
"subdomain wildcard doesn't match domain hijack attempt",
"https://*.example.com/callback",
"https://malicious.site?url=abc.example.com/callback",
false,
},
{
"hostname mismatch with confusable characters",
"https://example.com/callback",
"https://examp1e.com/callback",
false,
},
{
"hostname mismatch with homograph attack",
"https://example.com/callback",
"https://еxample.com/callback",
false,
},
// Port
{
"port mismatch",
"https://example.com:8080/callback",
"https://example.com:9090/callback",
false,
},
{
"wildcard port",
"https://example.com:*/callback",
"https://example.com:8080/callback",
true,
},
{
"partial wildcard in port prefix",
"https://example.com:80*/callback",
"https://example.com:8080/callback",
true,
},
// Path
{
"path mismatch",
"https://example.com/callback",
"https://example.com/other",
false,
},
{
"wildcard path segment",
"https://example.com/api/*/callback",
"https://example.com/api/v1/callback",
true,
},
{
"wildcard entire path",
"https://example.com/*",
"https://example.com/callback",
true,
},
{
"partial wildcard in path prefix",
"https://example.com/test*",
"https://example.com/testcase",
true,
},
{
"partial wildcard in path suffix",
"https://example.com/*-callback",
"https://example.com/oauth-callback",
true,
},
{
"partial wildcard in path middle",
"https://example.com/api-*-v1/callback",
"https://example.com/api-internal-v1/callback",
true,
},
{
"multiple partial wildcards in path",
"https://example.com/*/test*/callback",
"https://example.com/v1/testing/callback",
true,
},
{
"multiple wildcard segments in path",
"https://example.com/**/callback",
"https://example.com/api/v1/foo/bar/callback",
true,
},
{
"multiple wildcard segments in path",
"https://example.com/**/v1/**/callback",
"https://example.com/api/v1/foo/bar/callback",
true,
},
{
"partial wildcard matching full path segment",
"https://example.com/foo-*",
"https://example.com/foo-bar",
true,
},
// Credentials
{
"username mismatch",
"https://user:pass@example.com/callback",
"https://admin:pass@example.com/callback",
false,
},
{
"missing credentials",
"https://user:pass@example.com/callback",
"https://example.com/callback",
false,
},
{
"unexpected credentials",
"https://example.com/callback",
"https://user:pass@example.com/callback",
false,
},
{
"wildcard password",
"https://user:*@example.com/callback",
"https://user:secret123@example.com/callback",
true,
},
{
"partial wildcard in username",
"https://admin*:pass@example.com/callback",
"https://admin123:pass@example.com/callback",
true,
},
{
"partial wildcard in password",
"https://user:pass*@example.com/callback",
"https://user:password123@example.com/callback",
true,
},
{
"wildcard password doesn't allow domain hijack",
"https://user:*@example.com/callback",
"https://user:password@malicious.site#example.com/callback",
false,
},
{
"credentials with @ in password trying to hijack hostname",
"https://user:pass@example.com/callback",
"https://user:pass@evil.com@example.com/callback",
false,
},
// Query parameters
{
"extra query parameter",
"https://example.com/callback?code=*",
"https://example.com/callback?code=abc123&extra=value",
false,
},
{
"missing query parameter",
"https://example.com/callback?code=*&state=*",
"https://example.com/callback?code=abc123",
false,
},
{
"query parameter after fragment",
"https://example.com/callback?code=123",
"https://example.com/callback#section?code=123",
false,
},
{
"query parameter name mismatch",
"https://example.com/callback?code=*",
"https://example.com/callback?token=abc123",
false,
},
{
"wildcard query parameter",
"https://example.com/callback?code=*",
"https://example.com/callback?code=abc123",
true,
},
{
"multiple query parameters",
"https://example.com/callback?code=*&state=*",
"https://example.com/callback?code=abc123&state=xyz789",
true,
},
{
"query parameters in different order",
"https://example.com/callback?state=*&code=*",
"https://example.com/callback?code=abc123&state=xyz789",
true,
},
{
"exact query parameter value",
"https://example.com/callback?mode=production",
"https://example.com/callback?mode=production",
true,
},
{
"query parameter value mismatch",
"https://example.com/callback?mode=production",
"https://example.com/callback?mode=development",
false,
},
{
"mixed exact and wildcard query parameters",
"https://example.com/callback?mode=production&code=*",
"https://example.com/callback?mode=production&code=abc123",
true,
},
{
"mixed exact and wildcard with wrong exact value",
"https://example.com/callback?mode=production&code=*",
"https://example.com/callback?mode=development&code=abc123",
false,
},
{
"multiple values for same parameter",
"https://example.com/callback?param=*&param=*",
"https://example.com/callback?param=value1&param=value2",
true,
},
{
"unexpected query parameters",
"https://example.com/callback",
"https://example.com/callback?extra=value",
false,
},
{
"query parameter with redirect to external site",
"https://example.com/callback?code=*",
"https://example.com/callback?code=123&redirect=https://evil.com",
false,
},
{
"open redirect via encoded URL in query param",
"https://example.com/callback?state=*",
"https://example.com/callback?state=abc&next=//evil.com",
false,
},
// Fragment
{
"fragment ignored when both pattern and input have fragment",
"https://example.com/callback#fragment",
"https://example.com/callback#fragment",
true,
},
{
"fragment ignored when pattern has fragment but input doesn't",
"https://example.com/callback#fragment",
"https://example.com/callback",
true,
},
{
"fragment ignored when input has fragment but pattern doesn't",
"https://example.com/callback",
"https://example.com/callback#section",
true,
},
// Path traversal and injection attempts
{
"path traversal attempt",
"https://example.com/callback",
"https://example.com/../admin/callback",
false,
},
{
"backslash instead of forward slash",
"https://example.com/callback",
"https://example.com\\callback",
false,
},
{
"double slash in hostname (protocol smuggling)",
"https://example.com/callback",
"https://example.com//evil.com/callback",
false,
},
{
"CRLF injection attempt in path",
"https://example.com/callback",
"https://example.com/callback%0d%0aLocation:%20https://evil.com",
false,
},
{
"null byte injection",
"https://example.com/callback",
"https://example.com/callback%00.evil.com",
false,
},
}
for _, tt := range tests {
matches, err := matchCallbackURL(tt.pattern, tt.input)
require.NoError(t, err, tt.name)
assert.Equal(t, tt.shouldMatch, matches, tt.name)
}
}
func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
tests := []struct {
name string
urls []string
inputCallbackURL string
expectedURL string
expectMatch bool
}{
{
name: "127.0.0.1 with dynamic port - exact match",
urls: []string{"http://127.0.0.1/callback"},
inputCallbackURL: "http://127.0.0.1:8080/callback",
expectedURL: "http://127.0.0.1:8080/callback",
expectMatch: true,
},
{
name: "127.0.0.1 with different port",
urls: []string{"http://127.0.0.1/callback"},
inputCallbackURL: "http://127.0.0.1:9999/callback",
expectedURL: "http://127.0.0.1:9999/callback",
expectMatch: true,
},
{
name: "IPv6 loopback with dynamic port",
urls: []string{"http://[::1]/callback"},
inputCallbackURL: "http://[::1]:8080/callback",
expectedURL: "http://[::1]:8080/callback",
expectMatch: true,
},
{
name: "IPv6 loopback without brackets in input",
urls: []string{"http://[::1]/callback"},
inputCallbackURL: "http://::1:8080/callback",
expectedURL: "http://::1:8080/callback",
expectMatch: true,
},
{
name: "localhost with dynamic port",
urls: []string{"http://localhost/callback"},
inputCallbackURL: "http://localhost:8080/callback",
expectedURL: "http://localhost:8080/callback",
expectMatch: true,
},
{
name: "https loopback doesn't trigger special handling",
urls: []string{"https://127.0.0.1/callback"},
inputCallbackURL: "https://127.0.0.1:8080/callback",
expectedURL: "",
expectMatch: false,
},
{
name: "loopback with path match",
urls: []string{"http://127.0.0.1/auth/*"},
inputCallbackURL: "http://127.0.0.1:3000/auth/callback",
expectedURL: "http://127.0.0.1:3000/auth/callback",
expectMatch: true,
},
{
name: "loopback with path mismatch",
urls: []string{"http://127.0.0.1/callback"},
inputCallbackURL: "http://127.0.0.1:8080/different",
expectedURL: "",
expectMatch: false,
},
{
name: "non-loopback IP",
urls: []string{"http://192.168.1.1/callback"},
inputCallbackURL: "http://192.168.1.1:8080/callback",
expectedURL: "",
expectMatch: false,
},
{
name: "wildcard matches loopback",
urls: []string{"*"},
inputCallbackURL: "http://127.0.0.1:8080/callback",
expectedURL: "http://127.0.0.1:8080/callback",
expectMatch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GetCallbackURLFromList(tt.urls, tt.inputCallbackURL)
require.NoError(t, err)
if tt.expectMatch {
assert.Equal(t, tt.expectedURL, result)
} else {
assert.Empty(t, result)
}
})
}
}
func TestGetCallbackURLFromList_MultiplePatterns(t *testing.T) {
tests := []struct {
name string
urls []string
inputCallbackURL string
expectedURL string
expectMatch bool
}{
{
name: "matches first pattern",
urls: []string{
"https://example.com/callback",
"https://example.org/callback",
},
inputCallbackURL: "https://example.com/callback",
expectedURL: "https://example.com/callback",
expectMatch: true,
},
{
name: "matches second pattern",
urls: []string{
"https://example.com/callback",
"https://example.org/callback",
},
inputCallbackURL: "https://example.org/callback",
expectedURL: "https://example.org/callback",
expectMatch: true,
},
{
name: "matches none",
urls: []string{
"https://example.com/callback",
"https://example.org/callback",
},
inputCallbackURL: "https://malicious.com/callback",
expectedURL: "",
expectMatch: false,
},
{
name: "matches wildcard pattern",
urls: []string{
"https://example.com/callback",
"https://*.example.org/callback",
},
inputCallbackURL: "https://subdomain.example.org/callback",
expectedURL: "https://subdomain.example.org/callback",
expectMatch: true,
},
{
name: "empty pattern list",
urls: []string{},
inputCallbackURL: "https://example.com/callback",
expectedURL: "",
expectMatch: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GetCallbackURLFromList(tt.urls, tt.inputCallbackURL)
require.NoError(t, err)
if tt.expectMatch {
assert.Equal(t, tt.expectedURL, result)
} else {
assert.Empty(t, result)
}
})
}
}
func TestMatchPath(t *testing.T) {
tests := []struct {
name string
pattern string
input string
shouldMatch bool
}{
// Exact matches
{
name: "exact match",
pattern: "/callback",
input: "/callback",
shouldMatch: true,
},
{
name: "exact mismatch",
pattern: "/callback",
input: "/other",
shouldMatch: false,
},
{
name: "empty paths",
pattern: "",
input: "",
shouldMatch: true,
},
// Single wildcard (*)
{
name: "single wildcard matches segment",
pattern: "/api/*/callback",
input: "/api/v1/callback",
shouldMatch: true,
},
{
name: "single wildcard doesn't match multiple segments",
pattern: "/api/*/callback",
input: "/api/v1/v2/callback",
shouldMatch: false,
},
{
name: "single wildcard at end",
pattern: "/callback/*",
input: "/callback/test",
shouldMatch: true,
},
{
name: "single wildcard at start",
pattern: "/*/callback",
input: "/api/callback",
shouldMatch: true,
},
{
name: "multiple single wildcards",
pattern: "/*/test/*",
input: "/api/test/callback",
shouldMatch: true,
},
{
name: "partial wildcard prefix",
pattern: "/test*",
input: "/testing",
shouldMatch: true,
},
{
name: "partial wildcard suffix",
pattern: "/*-callback",
input: "/oauth-callback",
shouldMatch: true,
},
{
name: "partial wildcard middle",
pattern: "/api-*-v1",
input: "/api-internal-v1",
shouldMatch: true,
},
// Double wildcard (**)
{
name: "double wildcard matches multiple segments",
pattern: "/api/**/callback",
input: "/api/v1/v2/v3/callback",
shouldMatch: true,
},
{
name: "double wildcard matches single segment",
pattern: "/api/**/callback",
input: "/api/v1/callback",
shouldMatch: true,
},
{
name: "double wildcard doesn't match when pattern has extra slashes",
pattern: "/api/**/callback",
input: "/api/callback",
shouldMatch: false,
},
{
name: "double wildcard at end",
pattern: "/api/**",
input: "/api/v1/v2/callback",
shouldMatch: true,
},
{
name: "double wildcard in middle",
pattern: "/api/**/v2/**/callback",
input: "/api/v1/v2/v3/v4/callback",
shouldMatch: true,
},
// Complex patterns
{
name: "mix of single and double wildcards",
pattern: "/*/api/**/callback",
input: "/app/api/v1/v2/callback",
shouldMatch: true,
},
{
name: "wildcard with special characters",
pattern: "/callback-*",
input: "/callback-123",
shouldMatch: true,
},
{
name: "path with query-like string (no special handling)",
pattern: "/callback?code=*",
input: "/callback?code=abc",
shouldMatch: true,
},
// Edge cases
{
name: "single wildcard matches empty segment",
pattern: "/api/*/callback",
input: "/api//callback",
shouldMatch: true,
},
{
name: "pattern longer than input",
pattern: "/api/v1/callback",
input: "/api",
shouldMatch: false,
},
{
name: "input longer than pattern",
pattern: "/api",
input: "/api/v1/callback",
shouldMatch: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matches, err := matchPath(tt.pattern, tt.input)
require.NoError(t, err)
assert.Equal(t, tt.shouldMatch, matches)
})
}
}
func TestSplitParts(t *testing.T) {
tests := []struct {
name string
input string
expectedParts []string
expectedPath string
}{
{
name: "simple https URL",
input: "https://example.com/callback",
expectedParts: []string{"https", "example", "com"},
expectedPath: "/callback",
},
{
name: "URL with port",
input: "https://example.com:8080/callback",
expectedParts: []string{"https", "example", "com", "8080"},
expectedPath: "/callback",
},
{
name: "URL with subdomain",
input: "https://api.example.com/callback",
expectedParts: []string{"https", "api", "example", "com"},
expectedPath: "/callback",
},
{
name: "URL with credentials",
input: "https://user:pass@example.com/callback",
expectedParts: []string{"https", "user", "pass", "example", "com"},
expectedPath: "/callback",
},
{
name: "URL without path",
input: "https://example.com",
expectedParts: []string{"https", "example", "com"},
expectedPath: "",
},
{
name: "URL with deep path",
input: "https://example.com/api/v1/callback",
expectedParts: []string{"https", "example", "com"},
expectedPath: "/api/v1/callback",
},
{
name: "URL with path and query",
input: "https://example.com/callback?code=123",
expectedParts: []string{"https", "example", "com"},
expectedPath: "/callback?code=123",
},
{
name: "URL with trailing slash",
input: "https://example.com/",
expectedParts: []string{"https", "example", "com"},
expectedPath: "/",
},
{
name: "URL with multiple subdomains",
input: "https://api.v1.staging.example.com/callback",
expectedParts: []string{"https", "api", "v1", "staging", "example", "com"},
expectedPath: "/callback",
},
{
name: "URL with port and credentials",
input: "https://user:pass@example.com:8080/callback",
expectedParts: []string{"https", "user", "pass", "example", "com", "8080"},
expectedPath: "/callback",
},
{
name: "scheme with authority separator but no slash",
input: "http://example.com",
expectedParts: []string{"http", "example", "com"},
expectedPath: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parts, path := splitParts(tt.input)
assert.Equal(t, tt.expectedParts, parts, "parts mismatch")
assert.Equal(t, tt.expectedPath, path, "path mismatch")
})
}
}

View File

@@ -1,8 +1,6 @@
package cookie
import (
"time"
"github.com/gin-gonic/gin"
)
@@ -13,7 +11,3 @@ func AddAccessTokenCookie(c *gin.Context, maxAgeInSeconds int, token string) {
func AddSessionIdCookie(c *gin.Context, maxAgeInSeconds int, sessionID string) {
c.SetCookie(SessionIdCookieName, sessionID, maxAgeInSeconds, "/", "", true, true)
}
func AddDeviceTokenCookie(c *gin.Context, deviceToken string) {
c.SetCookie(DeviceTokenCookieName, deviceToken, int(15*time.Minute.Seconds()), "/api/one-time-access-token", "", true, true)
}

View File

@@ -8,12 +8,10 @@ import (
var AccessTokenCookieName = "__Host-access_token"
var SessionIdCookieName = "__Host-session"
var DeviceTokenCookieName = "__Host-device_token" //nolint:gosec
func init() {
if strings.HasPrefix(common.EnvConfig.AppURL, "http://") {
AccessTokenCookieName = "access_token"
SessionIdCookieName = "session"
DeviceTokenCookieName = "device_token"
}
}

View File

@@ -1,21 +0,0 @@
package utils
import (
"context"
"time"
)
func SleepWithContext(ctx context.Context, delay time.Duration) error {
if delay <= 0 {
return nil
}
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}

View File

@@ -14,17 +14,6 @@ import (
// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
func GenerateRandomAlphanumericString(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return GenerateRandomString(length, charset)
}
// GenerateRandomUnambiguousString generates a random string of the given length using unambiguous characters
func GenerateRandomUnambiguousString(length int) (string, error) {
const charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789"
return GenerateRandomString(length, charset)
}
// GenerateRandomString generates a random string of the given length using the provided character set
func GenerateRandomString(length int, charset string) (string, error) {
if length <= 0 {
return "", errors.New("length must be a positive integer")

View File

@@ -2,7 +2,6 @@ package utils
import (
"regexp"
"strings"
"testing"
)
@@ -50,77 +49,6 @@ func TestGenerateRandomAlphanumericString(t *testing.T) {
})
}
func TestGenerateRandomUnambiguousString(t *testing.T) {
t.Run("valid length returns correct string", func(t *testing.T) {
const length = 10
str, err := GenerateRandomUnambiguousString(length)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if len(str) != length {
t.Errorf("Expected length %d, got %d", length, len(str))
}
matched, err := regexp.MatchString(`^[abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789]+$`, str)
if err != nil {
t.Errorf("Regex match failed: %v", err)
}
if !matched {
t.Errorf("String contains ambiguous characters: %s", str)
}
})
t.Run("zero length returns error", func(t *testing.T) {
_, err := GenerateRandomUnambiguousString(0)
if err == nil {
t.Error("Expected error for zero length, got nil")
}
})
t.Run("negative length returns error", func(t *testing.T) {
_, err := GenerateRandomUnambiguousString(-1)
if err == nil {
t.Error("Expected error for negative length, got nil")
}
})
}
func TestGenerateRandomString(t *testing.T) {
t.Run("valid length returns characters from charset", func(t *testing.T) {
const length = 20
const charset = "abc"
str, err := GenerateRandomString(length, charset)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if len(str) != length {
t.Errorf("Expected length %d, got %d", length, len(str))
}
for _, r := range str {
if !strings.ContainsRune(charset, r) {
t.Fatalf("String contains character outside charset: %q", r)
}
}
})
t.Run("zero length returns error", func(t *testing.T) {
_, err := GenerateRandomString(0, "abc")
if err == nil {
t.Error("Expected error for zero length, got nil")
}
})
t.Run("negative length returns error", func(t *testing.T) {
_, err := GenerateRandomString(-1, "abc")
if err == nil {
t.Error("Expected error for negative length, got nil")
}
})
}
func TestCapitalizeFirstLetter(t *testing.T) {
tests := []struct {
name string

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 B

View File

@@ -0,0 +1 @@
-- No-op in Postgres

View File

@@ -0,0 +1 @@
-- No-op in Postgres

View File

@@ -0,0 +1 @@
DROP TABLE oidc_apis;

View File

@@ -0,0 +1,11 @@
CREATE TABLE oidc_apis
(
id UUID PRIMARY KEY NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
name TEXT NOT NULL,
identifier TEXT NOT NULL,
data JSONB NOT NULL
);
CREATE UNIQUE INDEX idx_oidc_apis_identifier_key ON oidc_apis(identifier);
CREATE INDEX idx_oidc_apis_name_key ON oidc_apis(name);

View File

@@ -1 +0,0 @@
ALTER TABLE one_time_access_tokens DROP COLUMN device_token;

View File

@@ -1 +0,0 @@
ALTER TABLE one_time_access_tokens ADD COLUMN device_token VARCHAR(16);

View File

@@ -1 +0,0 @@
DROP TABLE signup_tokens_user_groups;

View File

@@ -1,8 +0,0 @@
CREATE TABLE signup_tokens_user_groups
(
signup_token_id UUID NOT NULL,
user_group_id UUID NOT NULL,
PRIMARY KEY (signup_token_id, user_group_id),
FOREIGN KEY (signup_token_id) REFERENCES signup_tokens (id) ON DELETE CASCADE,
FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE
);

View File

@@ -1,3 +0,0 @@
-- This migration is part of v2
-- No-op in Postgres

View File

@@ -1,3 +0,0 @@
-- This migration is part of v2
-- No-op in Postgres

View File

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

View File

@@ -1,10 +0,0 @@
ALTER TABLE oidc_clients
ADD COLUMN is_group_restricted boolean NOT NULL DEFAULT false;
UPDATE oidc_clients oc
SET is_group_restricted =
EXISTS (
SELECT 1
FROM oidc_clients_allowed_user_groups a
WHERE a.oidc_client_id = oc.id
);

View File

@@ -1,3 +0,0 @@
DROP TABLE scim_service_providers;
ALTER TABLE users DROP COLUMN updated_at;
ALTER TABLE user_groups DROP COLUMN updated_at;

View File

@@ -1,15 +0,0 @@
CREATE TABLE scim_service_providers
(
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
endpoint TEXT NOT NULL,
token TEXT NOT NULL,
last_synced_at TIMESTAMPTZ,
oidc_client_id TEXT NOT NULL REFERENCES oidc_clients (id) ON DELETE CASCADE
);
ALTER TABLE users
ADD COLUMN updated_at TIMESTAMPTZ;
ALTER TABLE user_groups
ADD COLUMN updated_at TIMESTAMPTZ;

View File

@@ -0,0 +1 @@
DROP TABLE oidc_apis;

View File

@@ -0,0 +1,11 @@
CREATE TABLE oidc_apis
(
id UUID PRIMARY KEY NOT NULL,
created_at DATETIME NOT NULL,
name TEXT NOT NULL,
identifier TEXT NOT NULL,
data TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_oidc_apis_identifier_key ON oidc_apis(identifier);
CREATE INDEX idx_oidc_apis_name_key ON oidc_apis(name);

View File

@@ -1,7 +0,0 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE one_time_access_tokens DROP COLUMN device_token;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,7 +0,0 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE one_time_access_tokens ADD COLUMN device_token TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,7 +0,0 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE signup_tokens_user_groups;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,14 +0,0 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE signup_tokens_user_groups
(
signup_token_id TEXT NOT NULL,
user_group_id TEXT NOT NULL,
PRIMARY KEY (signup_token_id, user_group_id),
FOREIGN KEY (signup_token_id) REFERENCES signup_tokens (id) ON DELETE CASCADE,
FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

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

View File

@@ -1,13 +0,0 @@
PRAGMA foreign_keys= OFF;
BEGIN;
ALTER TABLE oidc_clients
ADD COLUMN is_group_restricted BOOLEAN NOT NULL DEFAULT 0;
UPDATE oidc_clients
SET is_group_restricted = (SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END
FROM oidc_clients_allowed_user_groups
WHERE oidc_clients_allowed_user_groups.oidc_client_id = oidc_clients.id);
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -1,9 +0,0 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE scim_service_providers;
ALTER TABLE users DROP COLUMN updated_at;
ALTER TABLE user_groups DROP COLUMN updated_at;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,22 +0,0 @@
PRAGMA foreign_keys= OFF;
BEGIN;
CREATE TABLE scim_service_providers
(
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
endpoint TEXT NOT NULL,
token TEXT NOT NULL,
last_synced_at DATETIME,
oidc_client_id TEXT NOT NULL,
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE
);
ALTER TABLE users
ADD COLUMN updated_at DATETIME;
ALTER TABLE user_groups
ADD COLUMN updated_at DATETIME;
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -10,16 +10,16 @@
},
"dependencies": {
"@react-email/components": "1.0.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@react-email/preview-server": "5.0.7",
"@types/node": "^24.10.4",
"@react-email/preview-server": "5.0.5",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"react-email": "5.0.7",
"tsx": "^4.21.0"
"react-email": "5.0.5",
"tsx": "^4.20.6"
}
}

View File

@@ -95,7 +95,7 @@
"settings": "Nastavení",
"update_pocket_id": "Aktualizovat Pocket ID",
"powered_by": "Poháněno pomocí",
"see_your_recent_account_activities": "Zobrazte aktivity svého účtu v rámci nastavené doby uchovávání.",
"see_your_account_activities_from_the_last_3_months": "Podívejte se na aktivity Vašeho účtu za poslední 3 měsíce.",
"time": "Čas",
"event": "Událost",
"approximate_location": "Přibližná poloha",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Jste si jisti, že chcete vytvořit nový client secret? Dosavadní bude zneplatněn.",
"generate": "Generovat",
"new_client_secret_created_successfully": "Nový client secret byl úspěšně vytvořen",
"allowed_user_groups_updated_successfully": "Povolené skupiny uživatelů byly úspěšně aktualizovány",
"oidc_client_name": "OIDC Klient {name}",
"client_id": "ID klienta",
"client_secret": "Tajný klíč",
"show_more_details": "Zobrazit další podrobnosti",
"allowed_user_groups": "Povolené skupiny uživatelů",
"allowed_user_groups_description": "Vyberte skupiny uživatelů, jejichž členové se mohou přihlásit k tomuto klientovi.",
"allowed_user_groups_status_unrestricted_description": "Nejsou použita žádná omezení uživatelských skupin. K tomuto klientovi se může přihlásit kterýkoli uživatel.",
"unrestrict": "Bez omezení",
"restrict": "Omezit",
"user_groups_restriction_updated_successfully": "Omezení uživatelských skupin bylo úspěšně aktualizováno",
"allowed_user_groups_updated_successfully": "Povolené skupiny uživatelů byly úspěšně aktualizovány",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Přidejte do tohoto klienta uživatelské skupiny, abyste omezili přístup pouze pro uživatele v těchto skupinách. Pokud nejsou vybrány žádné skupiny uživatelů, všichni uživatelé budou mít přístup k tomuto klientovi.",
"favicon": "Favicona",
"light_mode_logo": "Logo světlého režimu",
"dark_mode_logo": "Logo tmavého režimu",
"email_logo": "E-mailové logo",
"background_image": "Obrázek na pozadí",
"language": "Jazyk",
"reset_profile_picture_question": "Resetovat profilový obrázek?",
@@ -332,7 +327,7 @@
"all_clients": "Všichni klienti",
"all_locations": "Všechna místa",
"global_audit_log": "Globální protokol auditu",
"see_all_recent_account_activities": "Zobrazit aktivity účtu všech uživatelů během nastavené doby uchovávání.",
"see_all_account_activities_from_the_last_3_months": "Zobrazit veškerou aktivitu uživatele za poslední 3 měsíce.",
"token_sign_in": "Přihlášení tokenem",
"client_authorization": "Autorizace klienta",
"new_client_authorization": "Nová autorizace klienta",
@@ -354,8 +349,8 @@
"login_code_email_success": "Přihlašovací kód byl odeslán uživateli.",
"send_email": "Odeslat e-mail",
"show_code": "Zobrazit kód",
"callback_url_description": "URL adresy poskytnuté vaším klientem. Pokud pole ponecháte prázdné, budou automaticky přidány. Podporovány jsou <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>zástupné znaky</link>.",
"logout_callback_url_description": "URL adresy poskytnuté vaším klientem pro odhlášení. Podporovány jsou <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>zástupné znaky</link>.",
"callback_url_description": "URL poskytnuté vaším klientem. Bude automaticky přidáno, pokud necháte prázdné. Zástupné znaky (*) jsou podporovány, ale raději se jim vyhýbejte, pro lepší bezpečnost.",
"logout_callback_url_description": "URL poskytnuté klientem pro odhlášení. Klientské zástupné znaky (*) jsou podporovány, ale raději se jim vyhýbejte, pro lepší bezpečnost.",
"api_key_expiration": "Vypršení platnosti API klíče",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Pošlete uživateli e-mail, jakmile jejich API klíč brzy vyprší.",
"authorize_device": "Autorizovat zařízení",
@@ -474,29 +469,5 @@
"default_profile_picture": "Výchozí profilový obrázek",
"light": "Světlo",
"dark": "Tmavá",
"system": "Systém",
"signup_token_user_groups_description": "Automaticky přiřaďte tyto skupiny uživatelům, kteří se zaregistrují pomocí tohoto tokenu.",
"allowed_oidc_clients": "Povolené klienty OIDC",
"allowed_oidc_clients_description": "Vyberte klienty OIDC, ke kterým se mohou členové této skupiny uživatelů přihlašovat.",
"unrestrict_oidc_client": "Bez omezení {clientName}",
"confirm_unrestrict_oidc_client_description": "Opravdu chcete zrušit omezení klienta OIDC <b>{clientName}</b>? Tím se odstraní všechna přiřazení skupin pro tohoto klienta a přihlásit se bude moci kterýkoli uživatel.",
"allowed_oidc_clients_updated_successfully": "Povolené klienty OIDC úspěšně aktualizovány",
"yes": "Ano",
"no": "Ne",
"restricted": "Omezený",
"scim_provisioning": "SCIM Provisioning",
"scim_provisioning_description": "Provisioning SCIM vám umožňuje automaticky přidávat a odebírat uživatele a skupiny z vašeho klienta OIDC. Více informací najdete v <link href='https://pocket-id.org/docs/configuration/scim'>dokumentaci</link>.",
"scim_endpoint": "Koncový bod SCIM",
"scim_token": "SCIM token",
"last_successful_sync_at": "Poslední úspěšná synchronizace: {time}",
"scim_configuration_updated_successfully": "Konfigurace SCIM byla úspěšně aktualizována.",
"scim_enabled_successfully": "SCIM úspěšně aktivován.",
"scim_disabled_successfully": "SCIM byl úspěšně deaktivován.",
"disable_scim_provisioning": "Zakázat provisioning SCIM",
"disable_scim_provisioning_confirm_description": "Opravdu chcete deaktivovat provisioning SCIM pro <b>{clientName}</b>? Tím se zastaví veškeré automatické poskytování a odebrání oprávnění uživatelům a skupinám.",
"scim_sync_failed": "Synchronizace SCIM se nezdařila. Další informace najdete v protokolech serveru.",
"scim_sync_successful": "Synchronizace SCIM byla úspěšně dokončena.",
"save_and_sync": "Uložit a synchronizovat",
"scim_save_changes_description": "Před spuštěním synchronizace SCIM je nutné uložit změny. Chcete uložit nyní?",
"scopes": "Rozsah"
"system": "Systém"
}

View File

@@ -95,7 +95,7 @@
"settings": "Indstillinger",
"update_pocket_id": "Opdater Pocket ID",
"powered_by": "Drevet af",
"see_your_recent_account_activities": "Se dine kontoaktiviteter inden for den konfigurerede opbevaringsperiode.",
"see_your_account_activities_from_the_last_3_months": "Se dine kontoaktiviteter fra de sidste 3 måneder.",
"time": "Tid",
"event": "Hændelse",
"approximate_location": "Omtrentlig placering",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Vil du oprette en ny client secret? Den gamle bliver ugyldig og kan ikke længere bruges.",
"generate": "Generér",
"new_client_secret_created_successfully": "Ny client secret blev oprettet",
"allowed_user_groups_updated_successfully": "Tilladte brugergrupper blev opdateret",
"oidc_client_name": "OIDC-klient {name}",
"client_id": "Client ID",
"client_secret": "Client secret",
"show_more_details": "Vis flere detaljer",
"allowed_user_groups": "Tilladte brugergrupper",
"allowed_user_groups_description": "Vælg de brugergrupper, hvis medlemmer har tilladelse til at logge på denne klient.",
"allowed_user_groups_status_unrestricted_description": "Der gælder ingen begrænsninger for brugergrupper. Alle brugere kan logge på denne klient.",
"unrestrict": "Ubegrænset",
"restrict": "Begræns",
"user_groups_restriction_updated_successfully": "Begrænsning af brugergrupper opdateret med succes",
"allowed_user_groups_updated_successfully": "Tilladte brugergrupper blev opdateret",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Tilføj brugergrupper til denne klient for at begrænse adgangen til brugere i disse grupper. Hvis ingen brugergrupper er valgt, vil alle brugere have adgang til klienten.",
"favicon": "Favicon",
"light_mode_logo": "Logo til lys tilstand",
"dark_mode_logo": "Logo til mørk tilstand",
"email_logo": "E-mail-logo",
"background_image": "Baggrundsbillede",
"language": "Sprog",
"reset_profile_picture_question": "Nulstil profilbillede?",
@@ -332,7 +327,7 @@
"all_clients": "Alle klienter",
"all_locations": "Alle lokationer",
"global_audit_log": "Global aktivitetslog",
"see_all_recent_account_activities": "Se alle brugeres kontoaktiviteter i den angivne opbevaringsperiode.",
"see_all_account_activities_from_the_last_3_months": "Se al brugeraktivitet for de seneste 3 måneder.",
"token_sign_in": "Token-login",
"client_authorization": "Godkendelse af klient",
"new_client_authorization": "Ny klientgodkendelse",
@@ -354,8 +349,8 @@
"login_code_email_success": "Loginkoden er sendt til brugeren.",
"send_email": "Send e-mail",
"show_code": "Vis kode",
"callback_url_description": "URL(er) angivet af din klient. Tilføjes automatisk, hvis feltet efterlades tomt. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Jokertegn</link> understøttes.",
"logout_callback_url_description": "URL(er) angivet af din klient til logout. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Wildcards</link> understøttes.",
"callback_url_description": "En eller flere URLer angivet af din klient. Tilføjes automatisk, hvis feltet er tomt. Wildcards (*) understøttes, men bør undgås af hensyn til sikkerheden.",
"logout_callback_url_description": "En eller flere URLer angivet af din klient til logout. Wildcards (*) understøttes, men bør undgås af hensyn til sikkerheden.",
"api_key_expiration": "Udløb af API-nøgle",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send en e-mail til brugeren, når deres API-nøgle er ved at udløbe.",
"authorize_device": "Godkend enhed",
@@ -474,29 +469,5 @@
"default_profile_picture": "Standardprofilbillede",
"light": "Lys",
"dark": "Mørk",
"system": "System",
"signup_token_user_groups_description": "Tildel automatisk disse grupper til brugere, der tilmelder sig ved hjælp af denne token.",
"allowed_oidc_clients": "Tilladte OIDC-klienter",
"allowed_oidc_clients_description": "Vælg de OIDC-klienter, som medlemmer af denne brugergruppe har tilladelse til at logge på.",
"unrestrict_oidc_client": "Ubegrænset {clientName}",
"confirm_unrestrict_oidc_client_description": "Er du sikker på, at du vil ophæve begrænsningen for OIDC-klienten <b>{clientName}</b>? Dette vil fjerne alle gruppetildelinger for denne klient, og alle brugere vil kunne logge ind.",
"allowed_oidc_clients_updated_successfully": "Tilladte OIDC-klienter opdateret med succes",
"yes": "Ja",
"no": "Nej",
"restricted": "Begrænset",
"scim_provisioning": "SCIM-provisionering",
"scim_provisioning_description": "SCIM-provisionering giver dig mulighed for automatisk at provisionere og deprovisionere brugere og grupper fra din OIDC-klient. Få mere at vide i <link href='https://pocket-id.org/docs/configuration/scim'>dokumentationen</link>.",
"scim_endpoint": "SCIM-endpoint",
"scim_token": "SCIM-token",
"last_successful_sync_at": "Sidste vellykkede synkronisering: {time}",
"scim_configuration_updated_successfully": "SCIM-konfigurationen er opdateret.",
"scim_enabled_successfully": "SCIM er aktiveret med succes.",
"scim_disabled_successfully": "SCIM er deaktiveret.",
"disable_scim_provisioning": "Deaktiver SCIM-provisionering",
"disable_scim_provisioning_confirm_description": "Er du sikker på, at du vil deaktivere SCIM-provisionering for <b>{clientName}</b>? Dette vil stoppe al automatisk bruger- og gruppe-provisionering og deprovisionering.",
"scim_sync_failed": "SCIM-synkronisering mislykkedes. Se serverlogfilerne for yderligere oplysninger.",
"scim_sync_successful": "SCIM-synkroniseringen er gennemført med succes.",
"save_and_sync": "Gem og synkroniser",
"scim_save_changes_description": "Du skal gemme ændringerne, før du starter en SCIM-synkronisering. Vil du gemme nu?",
"scopes": "Omfang"
"system": "System"
}

View File

@@ -14,7 +14,7 @@
"profile_picture": "Profilbild",
"profile_picture_is_managed_by_ldap_server": "Das Profilbild wird vom LDAP-Server verwaltet und kann hier nicht geändert werden.",
"click_profile_picture_to_upload_custom": "Klicke auf das Profilbild, um ein benutzerdefiniertes Bild aus deinen Dateien hochzuladen.",
"image_should_be_in_format": "Das Bild sollte im PNG-, JPEG- oder WEBP-Format sein.",
"image_should_be_in_format": "Das Bild sollte im PNG-, JPEG- oder WEBP-Format vorliegen.",
"items_per_page": "Einträge pro Seite",
"no_items_found": "Keine Einträge gefunden",
"select_items": "Elemente auswählen...",
@@ -95,7 +95,7 @@
"settings": "Einstellungen",
"update_pocket_id": "Pocket ID aktualisieren",
"powered_by": "Powered by",
"see_your_recent_account_activities": "Schau dir deine Kontoaktivitäten innerhalb der eingestellten Aufbewahrungsfrist an.",
"see_your_account_activities_from_the_last_3_months": "Sieh dir deine Kontoaktivitäten der letzten drei Monate an.",
"time": "Zeit",
"event": "Ereignis",
"approximate_location": "Ungefährer Standort",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Bist du sicher, dass du ein neues Client-Geheimnis erstellen möchtest? Das alte Client-Geheimnis wird dadurch ungültig.",
"generate": "Generieren",
"new_client_secret_created_successfully": "Neues Client-Geheimnis erfolgreich erstellt",
"allowed_user_groups_updated_successfully": "Erlaubte Benutzergruppen erfolgreich aktualisiert",
"oidc_client_name": "OIDC-Client {name}",
"client_id": "Client-ID",
"client_secret": "Client-Geheimnis",
"show_more_details": "Mehr Details anzeigen",
"allowed_user_groups": "Erlaubte Benutzergruppen",
"allowed_user_groups_description": "Wähle die Benutzergruppen aus, deren Mitglieder sich bei diesem Client anmelden dürfen.",
"allowed_user_groups_status_unrestricted_description": "Es gibt keine Einschränkungen für Benutzergruppen. Jeder Benutzer kann sich bei diesem Client anmelden.",
"unrestrict": "Uneingeschränkt",
"restrict": "Einschränken",
"user_groups_restriction_updated_successfully": "Benutzergruppenbeschränkung erfolgreich aktualisiert",
"allowed_user_groups_updated_successfully": "Erlaubte Benutzergruppen erfolgreich aktualisiert",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Füge diesem Client Benutzergruppen hinzu, um den Zugriff auf Benutzer in diesen Gruppen zu beschränken. Wenn keine Benutzergruppen ausgewählt sind, werden alle Benutzer Zugriff auf diesen Client haben.",
"favicon": "Favicon",
"light_mode_logo": "Hell-Modus Logo",
"dark_mode_logo": "Dunkel-Modus Logo",
"email_logo": "E-Mail-Logo",
"background_image": "Hintergrundbild",
"language": "Sprache",
"reset_profile_picture_question": "Profilbild zurücksetzen?",
@@ -332,7 +327,7 @@
"all_clients": "Alle Clients",
"all_locations": "Alle Orte",
"global_audit_log": "Globaler Aktivitäts-Log",
"see_all_recent_account_activities": "Schau dir die Kontoaktivitäten von allen Nutzern während des festgelegten Aufbewahrungszeitraums an.",
"see_all_account_activities_from_the_last_3_months": "Sieh dir alle Benutzeraktivitäten der letzten 3 Monate an.",
"token_sign_in": "Token-Anmeldung",
"client_authorization": "Client-Autorisierung",
"new_client_authorization": "Neue Client-Autorisierung",
@@ -354,8 +349,8 @@
"login_code_email_success": "Der Login-Code wurde an den Benutzer gesendet.",
"send_email": "E-Mail senden",
"show_code": "Code anzeigen",
"callback_url_description": "Die URL(s) von deinem Kunden. Wenn du das Feld leer lässt, werden sie automatisch hinzugefügt. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Platzhalter</link> werden unterstützt.",
"logout_callback_url_description": "Von deinem Kunden angegebene URL(s) zum Abmelden. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Platzhalter</link> werden unterstützt.",
"callback_url_description": "URL(s) die von deinem Client bereitgestellt werden. Automatische Ergänzung bei leerem Feld. Wildcards (*) werden unterstützt, sollten für bessere Sicherheit jedoch vermieden werden.",
"logout_callback_url_description": "URL(s) die von deinem Client für die Abmeldung bereitgestellt werden. Wildcards (*) werden unterstützt, sollten für bessere Sicherheit jedoch vermieden werden.",
"api_key_expiration": "API-Schlüssel-Ablauf",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Sende eine E-Mail an den Benutzer, wenn sein API-Schlüssel ablaufen wird.",
"authorize_device": "Gerät autorisieren",
@@ -474,29 +469,5 @@
"default_profile_picture": "Standard-Profilbild",
"light": "Hell",
"dark": "Dunkel",
"system": "System",
"signup_token_user_groups_description": "Weise diese Gruppen automatisch den Leuten zu, die sich mit diesem Token anmelden.",
"allowed_oidc_clients": "Zugelassene OIDC-Clients",
"allowed_oidc_clients_description": "Wähle die OIDC-Clients aus, bei denen sich Mitglieder dieser Benutzergruppe anmelden dürfen.",
"unrestrict_oidc_client": "Uneingeschränkt {clientName}",
"confirm_unrestrict_oidc_client_description": "Bist du sicher, dass du die Beschränkung für den OIDC-Client aufheben willst? <b>{clientName}</b>? Dadurch werden alle Gruppenzuweisungen für diesen Client entfernt und jeder Benutzer kann sich anmelden.",
"allowed_oidc_clients_updated_successfully": "Zugelassene OIDC-Clients erfolgreich aktualisiert",
"yes": "Ja",
"no": "Nein",
"restricted": "Eingeschränkt",
"scim_provisioning": "SCIM-Bereitstellung",
"scim_provisioning_description": "Mit SCIM-Provisioning kannst du Benutzer und Gruppen automatisch über deinen OIDC-Client bereitstellen und deren Berechtigungen entziehen. Mehr Infos findest du in der <link href='https://pocket-id.org/docs/configuration/scim'>Dokumentation</link>.",
"scim_endpoint": "SCIM-Endpunkt",
"scim_token": "SCIM-Token",
"last_successful_sync_at": "Letzte erfolgreiche Synchronisierung: {time}",
"scim_configuration_updated_successfully": "SCIM-Konfiguration erfolgreich aktualisiert.",
"scim_enabled_successfully": "SCIM erfolgreich aktiviert.",
"scim_disabled_successfully": "SCIM wurde erfolgreich deaktiviert.",
"disable_scim_provisioning": "SCIM-Bereitstellung deaktivieren",
"disable_scim_provisioning_confirm_description": "Bist du sicher, dass du die SCIM-Bereitstellung für <b>{clientName}</b>? Dadurch werden alle automatischen Berechtigungen und Entzugungen für Benutzer und Gruppen gestoppt.",
"scim_sync_failed": "Die SCIM-Synchronisierung hat nicht geklappt. Schau mal in den Serverprotokollen nach, da findest du mehr Infos.",
"scim_sync_successful": "Die SCIM-Synchronisierung ist erfolgreich abgeschlossen worden.",
"save_and_sync": "Speichern und synchronisieren",
"scim_save_changes_description": "Du musst die Änderungen speichern, bevor du eine SCIM-Synchronisierung startest. Willst du jetzt speichern?",
"scopes": "Kopfsuchgeräte"
"system": "System"
}

View File

@@ -95,7 +95,7 @@
"settings": "Settings",
"update_pocket_id": "Update Pocket ID",
"powered_by": "Powered by",
"see_your_recent_account_activities": "See your account activities within the configured retention period.",
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
"time": "Time",
"event": "Event",
"approximate_location": "Approximate Location",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
"generate": "Generate",
"new_client_secret_created_successfully": "New client secret created successfully",
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
"oidc_client_name": "OIDC Client {name}",
"client_id": "Client ID",
"client_secret": "Client secret",
"show_more_details": "Show more details",
"allowed_user_groups": "Allowed User Groups",
"allowed_user_groups_description": "Select the user groups whose members are allowed to sign in to this client.",
"allowed_user_groups_status_unrestricted_description": "No user group restrictions are applied. Any user can sign in to this client.",
"unrestrict": "Unrestrict",
"restrict": "Restrict",
"user_groups_restriction_updated_successfully": "User groups restriction updated successfully",
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
"favicon": "Favicon",
"light_mode_logo": "Light Mode Logo",
"dark_mode_logo": "Dark Mode Logo",
"email_logo": "Email Logo",
"background_image": "Background Image",
"language": "Language",
"reset_profile_picture_question": "Reset profile picture?",
@@ -332,7 +327,7 @@
"all_clients": "All Clients",
"all_locations": "All Locations",
"global_audit_log": "Global Audit Log",
"see_all_recent_account_activities": "View the account activities of all users during the set retention period.",
"see_all_account_activities_from_the_last_3_months": "See all user activity for the last 3 months.",
"token_sign_in": "Token Sign In",
"client_authorization": "Client Authorization",
"new_client_authorization": "New Client Authorization",
@@ -354,8 +349,8 @@
"login_code_email_success": "The login code has been sent to the user.",
"send_email": "Send Email",
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Will be automatically added if left blank. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Wildcards</link> are supported.",
"logout_callback_url_description": "URL(s) provided by your client for logout. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Wildcards</link> are supported.",
"callback_url_description": "URL(s) provided by your client. Will be automatically added if left blank. Wildcards (*) are supported, but best avoided for better security.",
"logout_callback_url_description": "URL(s) provided by your client for logout. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
"authorize_device": "Authorize Device",
@@ -474,29 +469,5 @@
"default_profile_picture": "Default Profile Picture",
"light": "Light",
"dark": "Dark",
"system": "System",
"signup_token_user_groups_description": "Automatically assign these groups to users who sign up using this token.",
"allowed_oidc_clients": "Allowed OIDC Clients",
"allowed_oidc_clients_description": "Select the OIDC clients that members of this user group are allowed to sign in to.",
"unrestrict_oidc_client": "Unrestrict {clientName}",
"confirm_unrestrict_oidc_client_description": "Are you sure you want to unrestrict the OIDC client <b>{clientName}</b>? This will remove all group assignments for this client and any user will be able to sign in.",
"allowed_oidc_clients_updated_successfully": "Allowed OIDC clients updated successfully",
"yes": "Yes",
"no": "No",
"restricted": "Restricted",
"scim_provisioning": "SCIM Provisioning",
"scim_provisioning_description": "SCIM provisioning allows you to automatically provision and deprovision users and groups from your OIDC client. Learn more in the <link href='https://pocket-id.org/docs/configuration/scim'>docs</link>.",
"scim_endpoint": "SCIM Endpoint",
"scim_token": "SCIM Token",
"last_successful_sync_at": "Last successful sync: {time}",
"scim_configuration_updated_successfully": "SCIM configuration updated successfully.",
"scim_enabled_successfully": "SCIM enabled successfully.",
"scim_disabled_successfully": "SCIM disabled successfully.",
"disable_scim_provisioning": "Disable SCIM Provisioning",
"disable_scim_provisioning_confirm_description": "Are you sure you want to disable SCIM provisioning for <b>{clientName}</b>? This will stop all automatic user and group provisioning and deprovisioning.",
"scim_sync_failed": "SCIM sync failed. Check the server logs for more information.",
"scim_sync_successful": "The SCIM sync has been completed successfully.",
"save_and_sync": "Save and Sync",
"scim_save_changes_description": "You have to save the changes before starting a SCIM sync. Do you want to save now?",
"scopes": "Scopes"
"system": "System"
}

View File

@@ -95,7 +95,7 @@
"settings": "Configuración",
"update_pocket_id": "Actualizar Pocket ID",
"powered_by": "Producido por Pocket ID",
"see_your_recent_account_activities": "Consulta las actividades de tu cuenta dentro del período de retención configurado.",
"see_your_account_activities_from_the_last_3_months": "Vea las actividad de su cuenta de los últimos 3 meses.",
"time": "Tiempo",
"event": "Evento",
"approximate_location": "Ubicación aproximada",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "¿Estás seguro de que deseas crear un nuevo secreto de cliente? El antiguo quedará invalidado.",
"generate": "Generar",
"new_client_secret_created_successfully": "Se ha creado correctamente un nuevo secreto de cliente.",
"allowed_user_groups_updated_successfully": "Grupos de usuarios permitidos actualizados correctamente",
"oidc_client_name": "Cliente OIDC {name}",
"client_id": "ID de cliente",
"client_secret": "Secreto del cliente",
"show_more_details": "Mostrar más detalles",
"allowed_user_groups": "Grupos de usuarios permitidos",
"allowed_user_groups_description": "Selecciona los grupos de usuarios cuyos miembros pueden iniciar sesión en este cliente.",
"allowed_user_groups_status_unrestricted_description": "No se aplican restricciones de grupo de usuarios. Cualquier usuario puede iniciar sesión en este cliente.",
"unrestrict": "Sin restricciones",
"restrict": "Restringir",
"user_groups_restriction_updated_successfully": "Restricción de grupos de usuarios actualizada correctamente",
"allowed_user_groups_updated_successfully": "Grupos de usuarios permitidos actualizados correctamente",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Añade grupos de usuarios a este cliente para restringir el acceso a los usuarios de estos grupos. Si no se selecciona ningún grupo de usuarios, todos los usuarios tendrán acceso a este cliente.",
"favicon": "Favicon",
"light_mode_logo": "Logo del modo Claro",
"dark_mode_logo": "Logotipo en modo oscuro",
"email_logo": "Logotipo de correo electrónico",
"background_image": "Imagen de fondo",
"language": "Idioma",
"reset_profile_picture_question": "¿Restablecer foto de perfil?",
@@ -332,7 +327,7 @@
"all_clients": "Todos los clientes",
"all_locations": "Todas las ubicaciones",
"global_audit_log": "Registro de auditoría global",
"see_all_recent_account_activities": "Ver las actividades de la cuenta de todos los usuarios durante el periodo de retención establecido.",
"see_all_account_activities_from_the_last_3_months": "Ver toda la actividad de los usuarios durante los últimos 3 meses.",
"token_sign_in": "Inicio de sesión con token",
"client_authorization": "Autorización del cliente",
"new_client_authorization": "Autorización de nuevo cliente",
@@ -354,8 +349,8 @@
"login_code_email_success": "El código de inicio de sesión se ha enviado al usuario.",
"send_email": "Enviar correo electrónico",
"show_code": "Mostrar código",
"callback_url_description": "URL proporcionadas por tu cliente. Se añadirán automáticamente si se dejan en blanco. Se admiten <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>comodines</link>.",
"logout_callback_url_description": "URL proporcionadas por tu cliente para cerrar sesión. Se admiten <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>comodines</link>.",
"callback_url_description": "URL proporcionadas por tu cliente. Se añadirán automáticamente si se dejan en blanco. Se admiten comodines (*), pero es mejor evitarlos por motivos de seguridad.",
"logout_callback_url_description": "URL proporcionadas por tu cliente para cerrar sesión. Se admiten comodines (*), pero es mejor evitarlos para mayor seguridad.",
"api_key_expiration": "Caducidad de la clave API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envía un correo electrónico al usuario cuando tu clave API esté a punto de caducar.",
"authorize_device": "Autorizar dispositivo",
@@ -474,29 +469,5 @@
"default_profile_picture": "Imagen de perfil predeterminada",
"light": "Luz",
"dark": "Oscuro",
"system": "Sistema",
"signup_token_user_groups_description": "Asignad automáticamente estos grupos a los usuarios que se registren utilizando este token.",
"allowed_oidc_clients": "Clientes OIDC permitidos",
"allowed_oidc_clients_description": "Selecciona los clientes OIDC en los que los miembros de este grupo de usuarios pueden iniciar sesión.",
"unrestrict_oidc_client": "Sin restricciones {clientName}",
"confirm_unrestrict_oidc_client_description": "¿Estás seguro de que deseas eliminar las restricciones del cliente OIDC <b>{clientName}</b>? Esto eliminará todas las asignaciones de grupo para este cliente y cualquier usuario podrá iniciar sesión.",
"allowed_oidc_clients_updated_successfully": "Clientes OIDC permitidos actualizados correctamente",
"yes": "Sí",
"no": "No",
"restricted": "Restringido",
"scim_provisioning": "Aprovisionamiento SCIM",
"scim_provisioning_description": "El aprovisionamiento SCIM te permite aprovisionar y desaprovisionar automáticamente usuarios y grupos desde tu cliente OIDC. Obtén más información en la <link href='https://pocket-id.org/docs/configuration/scim'>documentación</link>.",
"scim_endpoint": "Punto final SCIM",
"scim_token": "Token SCIM",
"last_successful_sync_at": "Última sincronización correcta: {time}",
"scim_configuration_updated_successfully": "Configuración SCIM actualizada correctamente.",
"scim_enabled_successfully": "SCIM habilitado correctamente.",
"scim_disabled_successfully": "SCIM desactivado correctamente.",
"disable_scim_provisioning": "Desactivar el aprovisionamiento SCIM",
"disable_scim_provisioning_confirm_description": "¿Estás seguro de que deseas desactivar el aprovisionamiento SCIM para <b>{clientName}</b>? Esto detendrá todo el aprovisionamiento y desaprovisionamiento automático de usuarios y grupos.",
"scim_sync_failed": "Error en la sincronización SCIM. Consulta los registros del servidor para obtener más información.",
"scim_sync_successful": "La sincronización SCIM se ha completado correctamente.",
"save_and_sync": "Guardar y sincronizar",
"scim_save_changes_description": "Debes guardar los cambios antes de iniciar una sincronización SCIM. ¿Deseas guardar ahora?",
"scopes": "Ámbitos"
"system": "Sistema"
}

View File

@@ -95,7 +95,7 @@
"settings": "Asetukset",
"update_pocket_id": "Päivitä Pocket ID",
"powered_by": "Voimanlähteenä",
"see_your_recent_account_activities": "Tarkastele tilisi tapahtumia määritetyn säilytysajan puitteissa.",
"see_your_account_activities_from_the_last_3_months": "Katso tilisi tapahtumat viimeisen 3 kuukauden ajalta.",
"time": "Aika",
"event": "Tapahtuma",
"approximate_location": "Arvioitu sijainti",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Haluatko varmasti luoda uuden asiakassalaisuuden? Vanha salaisuus mitätöidään.",
"generate": "Luo",
"new_client_secret_created_successfully": "Uusi asiakassalaisuus luotu onnistuneesti",
"allowed_user_groups_updated_successfully": "Sallitut käyttäjäryhmät päivitetty onnistuneesti",
"oidc_client_name": "OIDC-asiakas {name}",
"client_id": "Asiakas ID",
"client_secret": "Asiakkaan salaisuus",
"show_more_details": "Näytä lisätietoja",
"allowed_user_groups": "Sallitut käyttäjäryhmät",
"allowed_user_groups_description": "Valitse käyttäjäryhmät, joiden jäsenet saavat kirjautua tähän asiakkaaseen.",
"allowed_user_groups_status_unrestricted_description": "Käyttäjäryhmärajoituksia ei sovelleta. Kuka tahansa käyttäjä voi kirjautua tähän asiakkaaseen.",
"unrestrict": "Rajoittamaton",
"restrict": "Rajoita",
"user_groups_restriction_updated_successfully": "Käyttäjäryhmien rajoitukset päivitetty onnistuneesti",
"allowed_user_groups_updated_successfully": "Sallitut käyttäjäryhmät päivitetty onnistuneesti",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Lisää käyttäjäryhmiä tähän asiakkaaseen rajoittaaksesi pääsyn näiden ryhmien käyttäjille. Jos käyttäjäryhmiä ei ole valittu, kaikki käyttäjät pääsevät käyttämään tätä asiakasta.",
"favicon": "Sivustokuvake",
"light_mode_logo": "Vaalean tilan logo",
"dark_mode_logo": "Tumman tilan logo",
"email_logo": "Sähköpostin logo",
"background_image": "Taustakuva",
"language": "Kieli",
"reset_profile_picture_question": "Palautetaanko profiilikuva?",
@@ -332,7 +327,7 @@
"all_clients": "Kaikki asiakkaat",
"all_locations": "Kaikki sijainnit",
"global_audit_log": "Globaali tarkastusloki",
"see_all_recent_account_activities": "Tarkastele kaikkien käyttäjien tilitoimintoja asetetun säilytysajan kuluessa.",
"see_all_account_activities_from_the_last_3_months": "Katso kaikkien käyttäjien toiminnot viimeisen 3 kuukauden ajalta.",
"token_sign_in": "Tunnuksella kirjautuminen",
"client_authorization": "Asiakkaan valtuutus",
"new_client_authorization": "Uuden asiakkaan valtuutus",
@@ -354,8 +349,8 @@
"login_code_email_success": "Pääsyavain poistettiin",
"send_email": "Lähetä sähköposti",
"show_code": "Näytä koodi",
"callback_url_description": "Asiakkaasi toimittamat URL-osoitteet. Lisätään automaattisesti, jos kenttä jätetään tyhjäksi. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Villikortit</link> ovat tuettuja.",
"logout_callback_url_description": "Asiakkaasi antamat URL-osoitteet kirjautumista varten. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Villikortit</link> ovat tuettuja.",
"callback_url_description": "Asiakkaasi antamat URL-osoitteet. Lisätään automaattisesti, jos kenttä jätetään tyhjäksi. Villikortit (*) ovat tuettuja, mutta niitä on parempi välttää turvallisuussyistä.",
"logout_callback_url_description": "Asiakkaasi antamat URL-osoitteet kirjautumiseen. Villikortit (*) ovat tuettuja, mutta niitä on parempi välttää turvallisuuden vuoksi.",
"api_key_expiration": "API-avaimen voimassaolon päättyminen",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Lähetä käyttäjälle sähköpostiviesti, kun hänen API-avaimensa on vanhentumassa.",
"authorize_device": "Valtuuta laite",
@@ -474,29 +469,5 @@
"default_profile_picture": "Oletusprofiilikuva",
"light": "Vaalea",
"dark": "Tumma",
"system": "Järjestelmä",
"signup_token_user_groups_description": "Määritä nämä ryhmät automaattisesti käyttäjille, jotka rekisteröityvät tällä tunnuksella.",
"allowed_oidc_clients": "Sallitut OIDC-asiakkaat",
"allowed_oidc_clients_description": "Valitse OIDC-asiakkaat, joihin tämän käyttäjäryhmän jäsenet voivat kirjautua.",
"unrestrict_oidc_client": "Rajoittamaton {clientName}",
"confirm_unrestrict_oidc_client_description": "Haluatko varmasti poistaa OIDC-asiakkaan rajoitukset <b>{clientName}</b>? Tämä poistaa kaikki tämän asiakkaan ryhmämääritykset, ja kuka tahansa käyttäjä voi kirjautua sisään.",
"allowed_oidc_clients_updated_successfully": "Sallitut OIDC-asiakkaat päivitetty onnistuneesti",
"yes": "Kyllä",
"no": "Ei",
"restricted": "Rajoitettu",
"scim_provisioning": "SCIM-provisiointi",
"scim_provisioning_description": "SCIM-provisioinnin avulla voit automaattisesti provisioida ja poistaa provisioinnin käyttäjiltä ja ryhmiltä OIDC-asiakasohjelmasta. Lisätietoja on <link href='https://pocket-id.org/docs/configuration/scim'>dokumentaatiossa</link>.",
"scim_endpoint": "SCIM-päätepiste",
"scim_token": "SCIM-tunnus",
"last_successful_sync_at": "Viimeisin onnistunut synkronointi: {time}",
"scim_configuration_updated_successfully": "SCIM-määritykset päivitetty onnistuneesti.",
"scim_enabled_successfully": "SCIM on otettu käyttöön onnistuneesti.",
"scim_disabled_successfully": "SCIM on poistettu käytöstä onnistuneesti.",
"disable_scim_provisioning": "Poista SCIM-provisiointi käytöstä",
"disable_scim_provisioning_confirm_description": "Haluatko varmasti poistaa SCIM-provisioinnin käytöstä <b>{clientName}</b>? Tämä lopettaa kaiken automaattisen käyttäjien ja ryhmien provisioningin ja deprovisioningin.",
"scim_sync_failed": "SCIM-synkronointi epäonnistui. Tarkista lisätietoja palvelimen lokitiedostoista.",
"scim_sync_successful": "SCIM-synkronointi on suoritettu onnistuneesti.",
"save_and_sync": "Tallenna ja synkronoi",
"scim_save_changes_description": "Sinun on tallennettava muutokset ennen SCIM-synkronoinnin aloittamista. Haluatko tallentaa nyt?",
"scopes": "Käyttöalueet"
"system": "Järjestelmä"
}

View File

@@ -44,7 +44,7 @@
"error_occurred_with_authenticator": "Une erreur est survenue pendant l'authentification",
"authenticator_does_not_support_discoverable_credentials": "L'authentificateur ne prend pas en charge les identifiants découvrables",
"authenticator_does_not_support_resident_keys": "L'authentificateur ne prend pas en charge les clés résidentes",
"passkey_was_previously_registered": "Cette clé d'accès a déjà été enregistrée",
"passkey_was_previously_registered": "Cette clé d'accès à déjà été enregistré",
"authenticator_does_not_support_any_of_the_requested_algorithms": "L'authentificateur ne supporte aucun des algorithmes requis",
"authenticator_timed_out": "L'authentification a expiré",
"critical_error_occurred_contact_administrator": "Une erreur critique s'est produite. Veuillez contacter votre administrateur.",
@@ -95,7 +95,7 @@
"settings": "Paramètres",
"update_pocket_id": "Mettre à jour Pocket ID",
"powered_by": "Propulsé par",
"see_your_recent_account_activities": "Regarde ce qui se passe sur ton compte pendant la période de conservation que tu as choisie.",
"see_your_account_activities_from_the_last_3_months": "Consultez les activités de votre compte au cours des 3 derniers mois.",
"time": "Date et heure",
"event": "Événement",
"approximate_location": "Lieu approximatif",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Êtes-vous sûr de vouloir créer un nouveau secret client ? L'ancien secret sera invalidé.",
"generate": "Générer",
"new_client_secret_created_successfully": "Nouveau secret client créé avec succès",
"allowed_user_groups_updated_successfully": "Groupes d'utilisateurs autorisés mis à jour avec succès",
"oidc_client_name": "Client OIDC {name}",
"client_id": "ID du client",
"client_secret": "Secret client",
"show_more_details": "Afficher plus de détails",
"allowed_user_groups": "Groupes d'utilisateurs autorisés",
"allowed_user_groups_description": "Choisis les groupes d'utilisateurs dont les membres peuvent se connecter à ce client.",
"allowed_user_groups_status_unrestricted_description": "Aucune restriction de groupe d'utilisateurs n'est appliquée. N'importe quel utilisateur peut se connecter à ce client.",
"unrestrict": "Débloquer",
"restrict": "Limiter",
"user_groups_restriction_updated_successfully": "La restriction des groupes d'utilisateurs a été mise à jour sans problème.",
"allowed_user_groups_updated_successfully": "Groupes d'utilisateurs autorisés mis à jour avec succès",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Ajouter des groupes d'utilisateurs à ce client permet de restreindre l'accès aux utilisateurs de ces groupes. Si aucun groupe d'utilisateurs n'est sélectionné, tous les utilisateurs auront accès à ce client.",
"favicon": "Icône du site",
"light_mode_logo": "Logo du Mode Clair",
"dark_mode_logo": "Logo du Mode Sombre",
"email_logo": "Logo e-mail",
"background_image": "Image d'arrière-plan",
"language": "Langue",
"reset_profile_picture_question": "Réinitialiser la photo de profil ?",
@@ -332,7 +327,7 @@
"all_clients": "Tous les clients",
"all_locations": "Tous les emplacements",
"global_audit_log": "Journal d'audit global",
"see_all_recent_account_activities": "Regarde ce que tous les utilisateurs ont fait sur leur compte pendant la période de conservation choisie.",
"see_all_account_activities_from_the_last_3_months": "Voir toutes les activités des utilisateurs des 3 derniers mois.",
"token_sign_in": "Connexion par jeton",
"client_authorization": "Autorisation client",
"new_client_authorization": "Nouvelle autorisation client",
@@ -354,8 +349,8 @@
"login_code_email_success": "Le code de connexion a été envoyé à l'utilisateur.",
"send_email": "Envoyer un email",
"show_code": "Afficher le code",
"callback_url_description": "URL fournies par ton client. Elles seront ajoutées automatiquement si tu ne remplis pas ce champ. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Les caractères génériques</link> sont pris en charge.",
"logout_callback_url_description": "URL fournies par ton client pour la déconnexion. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Les caractères génériques</link> sont pris en charge.",
"callback_url_description": "URL(s) fournies par votre client. Sera automatiquement ajoutée si laissée vide. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
"logout_callback_url_description": "URL(s) fournies par votre client pour la déconnexion. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
"api_key_expiration": "Expiration de la clé API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envoyer un email à l'utilisateur lorsque sa clé d'API est sur le point d'expirer.",
"authorize_device": "Autoriser l'appareil",
@@ -472,31 +467,7 @@
"reauthentication": "Réauthentification",
"clear_filters": "Effacer les filtres",
"default_profile_picture": "Photo de profil par défaut",
"light": "Clair",
"light": "Lumière",
"dark": "Sombre",
"system": "Système",
"signup_token_user_groups_description": "Attribuez automatiquement ces groupes aux utilisateurs qui s'inscrivent en utilisant ce jeton.",
"allowed_oidc_clients": "Clients OIDC autorisés",
"allowed_oidc_clients_description": "Choisissez les clients OIDC auxquels les membres de ce groupe d'utilisateurs peuvent se connecter.",
"unrestrict_oidc_client": "Débloquer {clientName}",
"confirm_unrestrict_oidc_client_description": "Tu es sûr de vouloir supprimer les restrictions pour le client OIDC <b>{clientName}</b>? Ça va supprimer toutes les affectations de groupe pour ce client et n'importe quel utilisateur pourra se connecter.",
"allowed_oidc_clients_updated_successfully": "Les clients OIDC autorisés ont été mis à jour sans problème.",
"yes": "Oui",
"no": "Non",
"restricted": "Limité",
"scim_provisioning": "Approvisionnement SCIM",
"scim_provisioning_description": "Le provisionnement SCIM te permet de provisionner et de déprovisionner automatiquement des utilisateurs et des groupes depuis ton client OIDC. Pour en savoir plus, jette un œil à la <link href='https://pocket-id.org/docs/configuration/scim'>documentation</link>.",
"scim_endpoint": "Point de terminaison SCIM",
"scim_token": "Jeton SCIM",
"last_successful_sync_at": "Dernière synchronisation réussie : {time}",
"scim_configuration_updated_successfully": "La configuration SCIM a été mise à jour sans problème.",
"scim_enabled_successfully": "SCIM activé avec succès.",
"scim_disabled_successfully": "SCIM désactivé, ça marche.",
"disable_scim_provisioning": "Désactiver l'approvisionnement SCIM",
"disable_scim_provisioning_confirm_description": "Tu es sûr de vouloir désactiver l'approvisionnement SCIM pour <b>{clientName}</b>? Ça va arrêter tout provisionnement et déprovisionnement automatique des utilisateurs et des groupes.",
"scim_sync_failed": "La synchronisation SCIM a échoué. Regarde dans les journaux du serveur pour plus d'infos.",
"scim_sync_successful": "La synchronisation SCIM s'est bien passée.",
"save_and_sync": "Enregistrer et synchroniser",
"scim_save_changes_description": "Tu dois enregistrer les changements avant de lancer une synchronisation SCIM. Tu veux enregistrer maintenant ?",
"scopes": "Portées"
"system": "Système"
}

View File

@@ -95,7 +95,7 @@
"settings": "Impostazioni",
"update_pocket_id": "Aggiorna Pocket ID",
"powered_by": "Powered by",
"see_your_recent_account_activities": "Guarda cosa succede nel tuo account durante il periodo di conservazione che hai impostato.",
"see_your_account_activities_from_the_last_3_months": "Visualizza le attività del tuo account degli ultimi 3 mesi.",
"time": "Ora",
"event": "Evento",
"approximate_location": "Posizione approssimativa",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Sei sicuro di voler creare un nuovo client secret? Quello vecchio sarà invalidato.",
"generate": "Genera",
"new_client_secret_created_successfully": "Nuovo client secret creato con successo",
"allowed_user_groups_updated_successfully": "Gruppi utente consentiti aggiornati con successo",
"oidc_client_name": "Client OIDC {name}",
"client_id": "ID client",
"client_secret": "Client secret",
"show_more_details": "Mostra più dettagli",
"allowed_user_groups": "Gruppi utente consentiti",
"allowed_user_groups_description": "Scegli i gruppi di utenti che possono accedere a questo client.",
"allowed_user_groups_status_unrestricted_description": "Non ci sono restrizioni per i gruppi di utenti. Chiunque può accedere a questo client.",
"unrestrict": "Sbloccare",
"restrict": "Limitare",
"user_groups_restriction_updated_successfully": "Restrizione dei gruppi di utenti aggiornata con successo",
"allowed_user_groups_updated_successfully": "Gruppi utente consentiti aggiornati con successo",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Aggiungi gruppi utente a questo client per limitare l'accesso agli utenti in questi gruppi. Se non viene selezionato alcun gruppo utente, tutti gli utenti avranno accesso a questo client.",
"favicon": "Favicon",
"light_mode_logo": "Logo modalità chiara",
"dark_mode_logo": "Logo modalità scura",
"email_logo": "Logo e-mail",
"background_image": "Immagine di sfondo",
"language": "Lingua",
"reset_profile_picture_question": "Reimpostare l'immagine del profilo?",
@@ -332,7 +327,7 @@
"all_clients": "Tutti i client",
"all_locations": "Tutte le posizioni",
"global_audit_log": "Registro attività globale",
"see_all_recent_account_activities": "Guarda cosa hanno fatto tutti gli utenti nei loro account durante il periodo di conservazione che hai scelto.",
"see_all_account_activities_from_the_last_3_months": "Visualizza tutte le attività degli utenti degli ultimi 3 mesi.",
"token_sign_in": "Accesso con token",
"client_authorization": "Autorizzazione client",
"new_client_authorization": "Nuova autorizzazione client",
@@ -354,8 +349,8 @@
"login_code_email_success": "Il codice di accesso è stato inviato all'utente.",
"send_email": "Invia email",
"show_code": "Mostra codice",
"callback_url_description": "URL forniti dal tuo cliente. Se lasci vuoto, verranno aggiunti automaticamente. Puoi usare <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>i caratteri jolly</link>.",
"logout_callback_url_description": "URL forniti dal tuo cliente per il logout. Si possono usare <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>i caratteri jolly</link>.",
"callback_url_description": "URL forniti dal client. Verrà automaticamente aggiunto se lasciato vuoto. I caratteri jolly (*) sono supportati, ma è meglio evitarli per maggiore sicurezza.",
"logout_callback_url_description": "URL forniti dal client per il logout. I caratteri jolly (*) sono supportati, ma meglio evitarli per una migliore sicurezza.",
"api_key_expiration": "Scadenza Chiave API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Invia un'email all'utente quando la sua chiave API sta per scadere.",
"authorize_device": "Autorizza Dispositivo",
@@ -474,29 +469,5 @@
"default_profile_picture": "Immagine del profilo predefinita",
"light": "Luce",
"dark": "Buio",
"system": "Sistema",
"signup_token_user_groups_description": "Assegna automaticamente questi gruppi agli utenti che si registrano usando questo token.",
"allowed_oidc_clients": "Client OIDC consentiti",
"allowed_oidc_clients_description": "Scegli i client OIDC a cui i membri di questo gruppo di utenti possono accedere.",
"unrestrict_oidc_client": "Sbloccare {clientName}",
"confirm_unrestrict_oidc_client_description": "Sei sicuro di voler rimuovere le restrizioni dal client OIDC <b>{clientName}</b>? Così facendo, cancellerai tutte le assegnazioni di gruppo per questo client e chiunque potrà accedere.",
"allowed_oidc_clients_updated_successfully": "I client OIDC autorizzati sono stati aggiornati senza problemi.",
"yes": "Sì",
"no": "No",
"restricted": "Limitato",
"scim_provisioning": "Provisioning SCIM",
"scim_provisioning_description": "Il provisioning SCIM ti permette di aggiungere e rimuovere automaticamente utenti e gruppi dal tuo client OIDC. Scopri di più nella <link href='https://pocket-id.org/docs/configuration/scim'>documentazione.</link>",
"scim_endpoint": "Endpoint SCIM",
"scim_token": "Token SCIM",
"last_successful_sync_at": "Ultima sincronizzazione riuscita: {time}",
"scim_configuration_updated_successfully": "Configurazione SCIM aggiornata senza problemi.",
"scim_enabled_successfully": "SCIM attivato con successo.",
"scim_disabled_successfully": "SCIM disattivato con successo.",
"disable_scim_provisioning": "Disattiva il provisioning SCIM",
"disable_scim_provisioning_confirm_description": "Sei sicuro di voler disattivare il provisioning SCIM per <b>{clientName}</b>? Questo fermerà tutto il provisioning e il deprovisioning automatico di utenti e gruppi.",
"scim_sync_failed": "La sincronizzazione SCIM non è andata a buon fine. Dai un'occhiata ai log del server per saperne di più.",
"scim_sync_successful": "La sincronizzazione SCIM è andata a buon fine.",
"save_and_sync": "Salva e sincronizza",
"scim_save_changes_description": "Devi salvare le modifiche prima di iniziare una sincronizzazione SCIM. Vuoi salvare adesso?",
"scopes": "Scopi"
"system": "Sistema"
}

View File

@@ -95,7 +95,7 @@
"settings": "設定",
"update_pocket_id": "Pocket ID を更新",
"powered_by": "Powered by",
"see_your_recent_account_activities": "設定された保持期間内のアカウント活動を確認できます。",
"see_your_account_activities_from_the_last_3_months": "過去3ヶ月間のアカウントアクティビティを確認する。",
"time": "時間",
"event": "イベント",
"approximate_location": "おおよその場所",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "新しいクライアントシークレットを作成してもよろしいですか?古いシークレットは無効化されます。",
"generate": "Generate",
"new_client_secret_created_successfully": "新しいクライアントシークレットが正常に作成されました",
"allowed_user_groups_updated_successfully": "許可されたユーザーグループが正常に更新されました",
"oidc_client_name": "OIDC クライアント {name}",
"client_id": "Client ID",
"client_secret": "クライアントシークレット",
"show_more_details": "詳細を表示",
"allowed_user_groups": "許可されたユーザーグループ",
"allowed_user_groups_description": "このクライアントにサインインを許可するユーザーグループを選択してください。",
"allowed_user_groups_status_unrestricted_description": "ユーザーグループの制限は適用されません。すべてのユーザーがこのクライアントにサインインできます。",
"unrestrict": "制限なし",
"restrict": "制限する",
"user_groups_restriction_updated_successfully": "ユーザーグループの制限が正常に更新されました",
"allowed_user_groups_updated_successfully": "許可されたユーザーグループが正常に更新されました",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
"favicon": "Favicon",
"light_mode_logo": "ライトモードのロゴ",
"dark_mode_logo": "ダークモードのロゴ",
"email_logo": "メール ロゴ",
"background_image": "背景画像",
"language": "言語",
"reset_profile_picture_question": "プロフィール画像をリセットしますか?",
@@ -332,7 +327,7 @@
"all_clients": "すべてのクライアント",
"all_locations": "すべての場所",
"global_audit_log": "グローバル監査ログ",
"see_all_recent_account_activities": "設定された保持期間中の全ユーザーのアカウント活動を閲覧する。",
"see_all_account_activities_from_the_last_3_months": "過去3ヶ月間のすべてのユーザーアクティビティを表示します。",
"token_sign_in": "トークンサインイン",
"client_authorization": "Client Authorization",
"new_client_authorization": "New Client Authorization",
@@ -354,8 +349,8 @@
"login_code_email_success": "ログインコードがユーザーに送信されました。",
"send_email": "メールを送信",
"show_code": "コードを表示",
"callback_url_description": "クライアントから提供されたURL。空白のままにすると自動的に追加されます。<link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>ワイルドカードが</link>サポートされています。",
"logout_callback_url_description": "クライアントから提供されたログアウト用URL。<link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>ワイルドカードが</link>サポートされています。",
"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": "デバイスの認証",
@@ -474,29 +469,5 @@
"default_profile_picture": "デフォルトのプロフィール画像",
"light": "光",
"dark": "闇",
"system": "システム",
"signup_token_user_groups_description": "このトークンを使用して登録するユーザーに、これらのグループを自動的に割り当てます。",
"allowed_oidc_clients": "許可されたOIDCクライアント",
"allowed_oidc_clients_description": "このユーザーグループのメンバーがサインインを許可されているOIDCクライアントを選択してください。",
"unrestrict_oidc_client": "制限なし {clientName}",
"confirm_unrestrict_oidc_client_description": "OIDCクライアントの制限を解除してもよろしいですか <b>{clientName}</b>?これにより、このクライアントに対するすべてのグループ割り当てが解除され、どのユーザーでもサインインできるようになります。",
"allowed_oidc_clients_updated_successfully": "許可されたOIDCクライアントの更新が正常に完了しました",
"yes": "はい",
"no": "いいえ",
"restricted": "制限付き",
"scim_provisioning": "SCIMプロビジョニング",
"scim_provisioning_description": "SCIMプロビジョニングにより、OIDCクライアントからユーザーやグループを自動的にプロビジョニングおよびデプロビジョニングできます。詳細は<link href='https://pocket-id.org/docs/configuration/scim'>ドキュメント</link>をご覧ください。",
"scim_endpoint": "SCIMエンドポイント",
"scim_token": "SCIMトークン",
"last_successful_sync_at": "最後の正常な同期: {time}",
"scim_configuration_updated_successfully": "SCIM設定の更新に成功しました。",
"scim_enabled_successfully": "SCIMが正常に有効化されました。",
"scim_disabled_successfully": "SCIMが無効化されました。",
"disable_scim_provisioning": "SCIMプロビジョニングを無効化",
"disable_scim_provisioning_confirm_description": "SCIMプロビジョニングを無効にすることを確認しますか <b>{clientName}</b>? これにより、すべての自動ユーザーおよびグループのプロビジョニングとデプロビジョニングが停止されます。",
"scim_sync_failed": "SCIM同期に失敗しました。詳細についてはサーバーログを確認してください。",
"scim_sync_successful": "SCIM同期が正常に完了しました。",
"save_and_sync": "保存と同期",
"scim_save_changes_description": "SCIM同期を開始する前に変更を保存する必要があります。今すぐ保存しますか",
"scopes": "スコープ"
"system": "システム"
}

View File

@@ -95,7 +95,7 @@
"settings": "설정",
"update_pocket_id": "Pocket ID 업데이트",
"powered_by": "제공:",
"see_your_recent_account_activities": "설정된 보존 기간 내의 계정 활동을 확인하세요.",
"see_your_account_activities_from_the_last_3_months": "지난 3개월 동안의 계정 활동을 확인하세요.",
"time": "시간",
"event": "이벤트",
"approximate_location": "대략적인 위치",
@@ -301,21 +301,16 @@
"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": "허용된 사용자 그룹",
"allowed_user_groups_description": "이 클라이언트에 로그인할 수 있는 사용자 그룹을 선택하십시오.",
"allowed_user_groups_status_unrestricted_description": "사용자 그룹 제한이 적용되지 않습니다. 모든 사용자가 이 클라이언트에 로그인할 수 있습니다.",
"unrestrict": "제한 해제",
"restrict": "제한하다",
"user_groups_restriction_updated_successfully": "사용자 그룹 제한이 성공적으로 업데이트되었습니다.",
"allowed_user_groups_updated_successfully": "허용된 사용자 그룹이 성공적으로 변경되었습니다",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "이 클라이언트에 사용자 그룹을 추가하여 해당 그룹의 사용자의 접근를 제한합니다. 사용자 그룹을 선택하지 않으면 모든 사용자가 이 클라이언트에 접근할 수 있습니다.",
"favicon": "파비콘",
"light_mode_logo": "라이트 모드 로고",
"dark_mode_logo": "다크 모드 로고",
"email_logo": "이메일 로고",
"background_image": "배경 이미지",
"language": "언어",
"reset_profile_picture_question": "프로필 사진을 재설정하시겠습니까?",
@@ -332,7 +327,7 @@
"all_clients": "모든 클라이언트",
"all_locations": "모든 위치",
"global_audit_log": "전체 활동 기록",
"see_all_recent_account_activities": "설정된 보존 기간 동안 모든 사용자의 계정 활동을 확인하십시오.",
"see_all_account_activities_from_the_last_3_months": "지난 3개월 동안 모든 사용자 활동을 확인하세요.",
"token_sign_in": "토큰 로그인",
"client_authorization": "클라이언트 승인",
"new_client_authorization": "새로운 클라이언트 승인",
@@ -354,8 +349,8 @@
"login_code_email_success": "로그인 코드가 사용자에게 전송되었습니다.",
"send_email": "이메일 전송",
"show_code": "코드 표시",
"callback_url_description": "고객이 제공한 URL. 비워두면 자동으로 추가됩니다. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>와일드카드가</link> 지원됩니다.",
"logout_callback_url_description": "로그아웃을 위해 클라이언트가 제공하는 URL(들). <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>와일드카드가</link> 지원됩니다.",
"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": "기기 승인",
@@ -474,29 +469,5 @@
"default_profile_picture": "기본 프로필 사진",
"light": "라이트",
"dark": "다크",
"system": "시스템",
"signup_token_user_groups_description": "이 토큰을 사용하여 가입하는 사용자에게 자동으로 이 그룹들을 할당합니다.",
"allowed_oidc_clients": "허용된 OIDC 클라이언트",
"allowed_oidc_clients_description": "이 사용자 그룹의 구성원이 로그인할 수 있는 OIDC 클라이언트를 선택하십시오.",
"unrestrict_oidc_client": "제한 해제 {clientName}",
"confirm_unrestrict_oidc_client_description": "OIDC 클라이언트의 제한을 해제하시겠습니까? <b>{clientName}</b>? 이 작업은 해당 클라이언트의 모든 그룹 할당을 제거하며, 모든 사용자가 로그인할 수 있게 됩니다.",
"allowed_oidc_clients_updated_successfully": "허용된 OIDC 클라이언트 업데이트 성공",
"yes": "네",
"no": "아니",
"restricted": "제한됨",
"scim_provisioning": "SCIM 프로비저닝",
"scim_provisioning_description": "SCIM 프로비저닝을 통해 OIDC 클라이언트에서 사용자 및 그룹을 자동으로 프로비저닝 및 디프로비저닝할 수 있습니다. 자세한 내용은 <link href='https://pocket-id.org/docs/configuration/scim'>문서를</link> 참조하세요.",
"scim_endpoint": "SCIM 엔드포인트",
"scim_token": "SCIM 토큰",
"last_successful_sync_at": "마지막 성공적인 동기화: {time}",
"scim_configuration_updated_successfully": "SCIM 구성이 성공적으로 업데이트되었습니다.",
"scim_enabled_successfully": "SCIM이 성공적으로 활성화되었습니다.",
"scim_disabled_successfully": "SCIM이 성공적으로 비활성화되었습니다.",
"disable_scim_provisioning": "SCIM 프로비저닝 비활성화",
"disable_scim_provisioning_confirm_description": "SCIM 프로비저닝을 비활성화하시겠습니까? <b>{clientName}</b>? 이 작업은 모든 자동 사용자 및 그룹 프로비저닝과 디프로비저닝을 중지합니다.",
"scim_sync_failed": "SCIM 동기화가 실패했습니다. 자세한 내용은 서버 로그를 확인하십시오.",
"scim_sync_successful": "SCIM 동기화가 성공적으로 완료되었습니다.",
"save_and_sync": "저장 및 동기화",
"scim_save_changes_description": "SCIM 동기화를 시작하기 전에 변경 사항을 저장해야 합니다. 지금 저장하시겠습니까?",
"scopes": "범위"
"system": "시스템"
}

View File

@@ -95,7 +95,7 @@
"settings": "Instellingen",
"update_pocket_id": "Pocket-ID bijwerken",
"powered_by": "Aangedreven door",
"see_your_recent_account_activities": "Bekijk je accountactiviteiten binnen de ingestelde bewaartermijn.",
"see_your_account_activities_from_the_last_3_months": "Bekijk je accountactiviteiten van de afgelopen 3 maanden.",
"time": "Tijd",
"event": "Activiteit",
"approximate_location": "Geschatte locatie",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Weet je zeker dat je een nieuw clientgeheim wilt aanmaken? Het 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_secret": "Clientgeheim",
"show_more_details": "Meer details weergeven",
"allowed_user_groups": "Toegestane gebruikersgroepen",
"allowed_user_groups_description": "Kies de gebruikersgroepen waarvan de leden zich bij deze client mogen aanmelden.",
"allowed_user_groups_status_unrestricted_description": "Er zijn geen beperkingen voor gebruikersgroepen. Iedereen kan gewoon inloggen op deze client.",
"unrestrict": "Onbeperkt",
"restrict": "Beperken",
"user_groups_restriction_updated_successfully": "Beperking voor gebruikersgroepen is bijgewerkt.",
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Voeg gebruikersgroepen toe aan deze client om de toegang tot gebruikers in deze groepen te beperken. Als er geen gebruikersgroepen zijn geselecteerd, hebben alle gebruikers toegang tot deze client.",
"favicon": "Favicon",
"light_mode_logo": "Lichte modus logo",
"dark_mode_logo": "Donkere modus logo",
"email_logo": "E-mail logo",
"background_image": "Achtergrondfoto",
"language": "Taal",
"reset_profile_picture_question": "Profielfoto opnieuw instellen?",
@@ -332,7 +327,7 @@
"all_clients": "Alle clients",
"all_locations": "Alle locaties",
"global_audit_log": "Globaal activiteitenlogboek",
"see_all_recent_account_activities": "Bekijk wat alle gebruikers hebben gedaan in hun account tijdens de periode die je hebt ingesteld.",
"see_all_account_activities_from_the_last_3_months": "Bekijk alle gebruikersactiviteit van de afgelopen 3 maanden.",
"token_sign_in": "Inloggen met token",
"client_authorization": "Client autorisatie",
"new_client_authorization": "Nieuwe clientautorisatie",
@@ -354,8 +349,8 @@
"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 je klant heeft gegeven. Als je dit veld leeg laat, worden ze automatisch toegevoegd. Je kunt <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>jokertekens</link> gebruiken.",
"logout_callback_url_description": "URL(s) die je klant geeft om uit te loggen. Je kunt ook <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>jokertekens</link> gebruiken.",
"callback_url_description": "URL's die de client heeft aangegeven. Als je dit leeg laat, worden ze automatisch toegevoegd. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je dat beter niet doen.",
"logout_callback_url_description": "URL's die je 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 e-mail naar de gebruiker als de geldigheid van hun API-sleutel bijna verloopt.",
"authorize_device": "Apparaat autoriseren",
@@ -474,29 +469,5 @@
"default_profile_picture": "Standaard profielfoto",
"light": "Licht",
"dark": "Donker",
"system": "Systeem",
"signup_token_user_groups_description": "Wijs deze groepen automatisch toe aan mensen die zich aanmelden met dit token.",
"allowed_oidc_clients": "Toegestane OIDC-clients",
"allowed_oidc_clients_description": "Kies de OIDC-clients waar leden van deze gebruikersgroep zich mogen aanmelden.",
"unrestrict_oidc_client": "Onbeperkt {clientName}",
"confirm_unrestrict_oidc_client_description": "Weet je zeker dat je de OIDC-client wilt vrijgeven? <b>{clientName}</b>? Hierdoor worden alle groepstoewijzingen voor deze client verwijderd en kan elke gebruiker zich aanmelden.",
"allowed_oidc_clients_updated_successfully": "Toegestane OIDC-clients zijn goed bijgewerkt",
"yes": "Ja",
"no": "Nee",
"restricted": "Beperkt",
"scim_provisioning": "SCIM-provisioning",
"scim_provisioning_description": "Met SCIM-provisioning kun je automatisch gebruikers en groepen toevoegen en verwijderen vanuit je OIDC-client. Check de <link href='https://pocket-id.org/docs/configuration/scim'>documentatie</link> voor meer info.",
"scim_endpoint": "SCIM-eindpunt",
"scim_token": "SCIM-token",
"last_successful_sync_at": "Laatste succesvolle synchronisatie: {time}",
"scim_configuration_updated_successfully": "SCIM-configuratie is goed bijgewerkt.",
"scim_enabled_successfully": "SCIM is nu ingeschakeld.",
"scim_disabled_successfully": "SCIM is nu uitgeschakeld.",
"disable_scim_provisioning": "Schakel SCIM-provisioning uit",
"disable_scim_provisioning_confirm_description": "Weet je zeker dat je SCIM-provisioning wilt uitschakelen voor <b>{clientName}</b>? Hierdoor worden alle automatische provisioning- en deprovisioning-processen voor gebruikers en groepen gestopt.",
"scim_sync_failed": "SCIM-synchronisatie is mislukt. Check de serverlogboeken voor meer info.",
"scim_sync_successful": "De SCIM-synchronisatie is goed gelukt.",
"save_and_sync": "Opslaan en synchroniseren",
"scim_save_changes_description": "Je moet de wijzigingen opslaan voordat je een SCIM-synchronisatie start. Wil je nu opslaan?",
"scopes": "Scopes"
"system": "Systeem"
}

View File

@@ -95,7 +95,7 @@
"settings": "Ustawienia",
"update_pocket_id": "Aktualizuj Pocket ID",
"powered_by": "Zasilane przez",
"see_your_recent_account_activities": "Zobacz aktywność swojego konta w skonfigurowanym okresie przechowywania danych.",
"see_your_account_activities_from_the_last_3_months": "Zobacz aktywności swojego konta z ostatnich 3 miesięcy.",
"time": "Czas",
"event": "Wydarzenie",
"approximate_location": "Przybliżona lokalizacja",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Czy na pewno chcesz utworzyć nowy tajny klucz klienta? Stary zostanie unieważniony.",
"generate": "Generuj",
"new_client_secret_created_successfully": "Sukces! Nowy tajny klucz klienta został utworzony.",
"allowed_user_groups_updated_successfully": "Sukces! Dozwolone grupy użytkowników zostały zaktualizowane.",
"oidc_client_name": "Klient OIDC {name}",
"client_id": "ID klienta",
"client_secret": "Tajny klucz klienta",
"show_more_details": "Pokaż więcej szczegółów",
"allowed_user_groups": "Dozwolone grupy użytkowników",
"allowed_user_groups_description": "Wybierz grupy użytkowników, których członkowie mogą logować się do tego klienta.",
"allowed_user_groups_status_unrestricted_description": "Nie obowiązują żadne ograniczenia dotyczące grup użytkowników. Każdy użytkownik może zalogować się do tego klienta.",
"unrestrict": "Bez ograniczeń",
"restrict": "Ogranicz",
"user_groups_restriction_updated_successfully": "Ograniczenia dla grup użytkowników zaktualizowane pomyślnie",
"allowed_user_groups_updated_successfully": "Sukces! Dozwolone grupy użytkowników zostały zaktualizowane.",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Dodaj grupy użytkowników do tego klienta, aby ograniczyć dostęp do użytkowników w tych grupach. Jeśli nie wybrano żadnych grup użytkowników, wszyscy użytkownicy będą mieli dostęp do tego klienta.",
"favicon": "Ikona ulubionych",
"light_mode_logo": "Logo w trybie jasnym",
"dark_mode_logo": "Logo w trybie ciemnym",
"email_logo": "Logo e-maila",
"background_image": "Obraz tła",
"language": "Język",
"reset_profile_picture_question": "Zresetować zdjęcie profilowe?",
@@ -332,7 +327,7 @@
"all_clients": "Wszyscy klienci",
"all_locations": "Wszystkie lokalizacje",
"global_audit_log": "Globalny dziennik audytu",
"see_all_recent_account_activities": "Wyświetlajcie aktywność wszystkich użytkowników na kontach w ustalonym okresie przechowywania danych.",
"see_all_account_activities_from_the_last_3_months": "Zobacz wszystkie działania użytkowników z ostatnich 3 miesięcy.",
"token_sign_in": "Logowanie za pomocą tokena",
"client_authorization": "Autoryzacja klienta",
"new_client_authorization": "Nowa autoryzacja klienta",
@@ -354,8 +349,8 @@
"login_code_email_success": "Kod logowania został wysłany do użytkownika.",
"send_email": "Wyślij e-mail",
"show_code": "Pokaż kod",
"callback_url_description": "Adresy URL podane przez ciebie. Jeśli pole pozostanie puste, zostaną one dodane automatycznie. Obsługiwane są <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>symbole wieloznaczne</link>.",
"logout_callback_url_description": "Adresy URL podane przez ciebie w celu wylogowania. Obsługiwane są <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>symbole wieloznaczne</link>.",
"callback_url_description": "Adresy URL podane przez klienta. Zostaną automatycznie dodane, jeśli pole pozostanie puste. Obsługiwane są symbole wieloznaczne (*), ale dla większego bezpieczeństwa najlepiej ich unikać.",
"logout_callback_url_description": "Adresy URL podane przez klienta do wylogowania. Obsługiwane są symbole wieloznaczne (*), ale dla większego bezpieczeństwa najlepiej ich unikać.",
"api_key_expiration": "Wygaszenie klucza API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Wyślij e-mail do użytkownika, gdy jego klucz API ma wygasnąć.",
"authorize_device": "Autoryzuj urządzenie",
@@ -474,29 +469,5 @@
"default_profile_picture": "Domyślne zdjęcie profilowe",
"light": "Światło",
"dark": "Ciemny",
"system": "System",
"signup_token_user_groups_description": "Automatycznie przypisujcie te grupy do użytkowników, którzy zarejestrują się przy użyciu tego tokenu.",
"allowed_oidc_clients": "Dozwoleni klienci OIDC",
"allowed_oidc_clients_description": "Wybierz klientów OIDC, do których członkowie tej grupy użytkowników mogą się logować.",
"unrestrict_oidc_client": "Bez ograniczeń {clientName}",
"confirm_unrestrict_oidc_client_description": "Czy na pewno chcesz znieść ograniczenia dla klienta OIDC <b>{clientName}</b>? Spowoduje to usunięcie wszystkich przypisanych do niego grup, a każdy użytkownik będzie mógł się zalogować.",
"allowed_oidc_clients_updated_successfully": "Dozwolone klienci OIDC zaktualizowani pomyślnie",
"yes": "Tak",
"no": "Nie",
"restricted": "Ograniczony",
"scim_provisioning": "SCIM Provisioning",
"scim_provisioning_description": "Provisioning SCIM umożliwia automatyczne dodawanie i usuwanie użytkowników oraz grup z klienta OIDC. Więcej informacji znajdziesz w <link href='https://pocket-id.org/docs/configuration/scim'>dokumentacji</link>.",
"scim_endpoint": "Punkt końcowy SCIM",
"scim_token": "Token SCIM",
"last_successful_sync_at": "Ostatnia udana synchronizacja: {time}",
"scim_configuration_updated_successfully": "Konfiguracja SCIM została pomyślnie zaktualizowana.",
"scim_enabled_successfully": "SCIM został pomyślnie włączony.",
"scim_disabled_successfully": "SCIM został pomyślnie wyłączony.",
"disable_scim_provisioning": "Wyłącz dostarczanie SCIM",
"disable_scim_provisioning_confirm_description": "Czy na pewno chcesz wyłączyć przydzielanie SCIM dla <b>{clientName}</b>? Spowoduje to zatrzymanie wszystkich automatycznych procesów przydzielania i odbierania uprawnień użytkownikom i grupom.",
"scim_sync_failed": "Synchronizacja SCIM nie powiodła się. Więcej informacji znajdziesz w logach serwera.",
"scim_sync_successful": "Synchronizacja SCIM została pomyślnie zakończona.",
"save_and_sync": "Zapisz i zsynchronizuj",
"scim_save_changes_description": "Przed rozpoczęciem synchronizacji SCIM należy zapisać zmiany. Czy chcesz zapisać teraz?",
"scopes": "Zakresy"
"system": "System"
}

View File

@@ -95,7 +95,7 @@
"settings": "Configurações",
"update_pocket_id": "Atualizar Pocket ID",
"powered_by": "Fornecido por",
"see_your_recent_account_activities": "Veja as atividades da sua conta dentro do período de retenção configurado.",
"see_your_account_activities_from_the_last_3_months": "Veja as atividades da conta nos últimos 3 meses.",
"time": "Hora",
"event": "Evento",
"approximate_location": "Localização Aproximada",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Tem certeza que deseja criar um segredo de cliente? O antigo será invalidado.",
"generate": "Gerar",
"new_client_secret_created_successfully": "Novo segredo do cliente criado com sucesso",
"allowed_user_groups_updated_successfully": "Grupos de usuários permitidos atualizados com sucesso",
"oidc_client_name": "Cliente OIDC {name}",
"client_id": "ID do cliente",
"client_secret": "Segredo do cliente",
"show_more_details": "Mostrar mais detalhes",
"allowed_user_groups": "Grupos de usuários permitidos",
"allowed_user_groups_description": "Escolha os grupos de usuários cujos membros podem entrar neste cliente.",
"allowed_user_groups_status_unrestricted_description": "Não tem restrições de grupo de usuários. Qualquer pessoa pode entrar nesse cliente.",
"unrestrict": "Desbloquear",
"restrict": "Restringir",
"user_groups_restriction_updated_successfully": "Restrição de grupos de usuários atualizada com sucesso",
"allowed_user_groups_updated_successfully": "Grupos de usuários permitidos atualizados com sucesso",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Adicione grupos de usuários a este cliente para restringir o acesso somente aos usuários que fazem parte destes grupos. Se nenhum grupo de usuários for selecionado, todos os usuários terão acesso a este cliente.",
"favicon": "Ícone de Favorito",
"light_mode_logo": "Logotipo do modo claro",
"dark_mode_logo": "Logotipo do Modo Escuro",
"email_logo": "Logotipo do e-mail",
"background_image": "Imagem de fundo",
"language": "Idioma",
"reset_profile_picture_question": "Redefinir a foto de perfil?",
@@ -332,7 +327,7 @@
"all_clients": "Todos os clientes",
"all_locations": "Todos os locais",
"global_audit_log": "Registro de auditoria global",
"see_all_recent_account_activities": "Veja as atividades da conta de todos os usuários durante o período de retenção definido.",
"see_all_account_activities_from_the_last_3_months": "Veja todas as atividades de todos os usuários nos últimos 3 meses.",
"token_sign_in": "Entrar com token",
"client_authorization": "Autorização do cliente",
"new_client_authorization": "Autorização de novo cliente",
@@ -354,8 +349,8 @@
"login_code_email_success": "O código de login foi enviado para o usuário.",
"send_email": "Enviar e-mail",
"show_code": "Mostrar código",
"callback_url_description": "URL(s) fornecido(s) pelo seu cliente. Será adicionado automaticamente se deixado em branco. São suportados <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>caracteres curinga</link>.",
"logout_callback_url_description": "URL(s) fornecida(s) pelo seu cliente para fazer logout. São aceitos <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>caracteres curinga</link>.",
"callback_url_description": "URL(s) providenciados pelo cliente. Será adicionado automaticamente se deixar em branco. Apesar de caracteres curinga (*) serem aceitos, para uma segurança maior, não é recomendado utiliza-los.",
"logout_callback_url_description": "URL(s) providenciados pelo cliente para sair da conta. Apesar de curingas (*) serem aceitos, para uma segurança maior, não é recomendado utiliza-los.",
"api_key_expiration": "Expiração da chave API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envia um e-mail para o usuário quando a chave API estiver próxima de expirar.",
"authorize_device": "Autorizar dispositivo",
@@ -474,29 +469,5 @@
"default_profile_picture": "Foto de perfil padrão",
"light": "Luz",
"dark": "Escuro",
"system": "Sistema",
"signup_token_user_groups_description": "Atribuir automaticamente esses grupos aos usuários que se inscreverem usando esse token.",
"allowed_oidc_clients": "Clientes OIDC permitidos",
"allowed_oidc_clients_description": "Escolha os clientes OIDC nos quais os membros desse grupo de usuários podem fazer login.",
"unrestrict_oidc_client": "Desbloquear {clientName}",
"confirm_unrestrict_oidc_client_description": "Tem certeza que quer desbloquear o cliente OIDC? <b>{clientName}</b>? Isso vai tirar todas as atribuições de grupo para esse cliente e qualquer usuário vai poder fazer login.",
"allowed_oidc_clients_updated_successfully": "Clientes OIDC autorizados atualizados com sucesso",
"yes": "Sim",
"no": "Não",
"restricted": "Restrito",
"scim_provisioning": "Provisionamento SCIM",
"scim_provisioning_description": "O provisionamento SCIM permite que você faça o provisionamento e o desprovisionamento automático de usuários e grupos a partir do seu cliente OIDC. Saiba mais nos <link href='https://pocket-id.org/docs/configuration/scim'>documentos.</link>",
"scim_endpoint": "Ponto final SCIM",
"scim_token": "Token SCIM",
"last_successful_sync_at": "Última sincronização bem-sucedida: {time}",
"scim_configuration_updated_successfully": "Configuração SCIM atualizada com sucesso.",
"scim_enabled_successfully": "SCIM ativado com sucesso.",
"scim_disabled_successfully": "SCIM desativado com sucesso.",
"disable_scim_provisioning": "Desativar provisionamento SCIM",
"disable_scim_provisioning_confirm_description": "Tem certeza que quer desativar o provisionamento SCIM para <b>{clientName}</b>? Isso vai parar todo o provisionamento e desprovisionamento automático de usuários e grupos.",
"scim_sync_failed": "Falha na sincronização SCIM. Dá uma olhada nos registros do servidor pra saber mais.",
"scim_sync_successful": "A sincronização SCIM foi concluída com sucesso.",
"save_and_sync": "Salvar e sincronizar",
"scim_save_changes_description": "Você precisa salvar as alterações antes de iniciar uma sincronização SCIM. Quer salvar agora?",
"scopes": "Âmbitos"
"system": "Sistema"
}

View File

@@ -95,7 +95,7 @@
"settings": "Настройки",
"update_pocket_id": "Обновите Pocket ID",
"powered_by": "Работает на",
"see_your_recent_account_activities": "Проверь, что происходит с твоей учетной записью в течение того времени, которое ты установил.",
"see_your_account_activities_from_the_last_3_months": "Смотрите активность своей учетной записи за последние 3 месяца.",
"time": "Время",
"event": "Событие",
"approximate_location": "Примерное местоположение",
@@ -301,21 +301,16 @@
"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": "Идентификатор клиента",
"client_secret": "Секрет клиента",
"show_more_details": "Показать больше деталей",
"allowed_user_groups": "Разрешенные группы пользователей",
"allowed_user_groups_description": "Выбери группы пользователей, члены которых могут входить в этот клиент.",
"allowed_user_groups_status_unrestricted_description": "Никаких ограничений по группам пользователей нет. Любой может зайти в этот клиент.",
"unrestrict": "Без ограничений",
"restrict": "Ограничить",
"user_groups_restriction_updated_successfully": "Ограничение групп пользователей обновлено успешно",
"allowed_user_groups_updated_successfully": "Разрешенные группы пользователей успешно обновлены",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Добавить группы пользователей к этому клиенту для ограничения доступа пользователей в этих группах. Если группы пользователей не выбраны, все пользователи будут иметь доступ к этому клиенту.",
"favicon": "Значок",
"light_mode_logo": "Логотип светлого режима",
"dark_mode_logo": "Логотип темного режима",
"email_logo": "Логотип электронной почты",
"background_image": "Фоновое изображение",
"language": "Язык",
"reset_profile_picture_question": "Сбросить изображение профиля?",
@@ -332,7 +327,7 @@
"all_clients": "Все клиенты",
"all_locations": "Все местоположения",
"global_audit_log": "Глобальный журнал аудита",
"see_all_recent_account_activities": "Просмотри, что делали все пользователи на аккаунтах за период, который ты выбрал.",
"see_all_account_activities_from_the_last_3_months": "Смотрите активность всех пользователей за последние 3 месяца.",
"token_sign_in": "Вход с помощью токена",
"client_authorization": "Авторизация клиента",
"new_client_authorization": "Авторизация нового клиента",
@@ -354,8 +349,8 @@
"login_code_email_success": "Код входа был отправлен пользователю.",
"send_email": "Отправить письмо",
"show_code": "Показать код",
"callback_url_description": "URL-адреса, которые дал твой клиент. Если поле оставить пустым, они добавятся автоматически. Поддерживаются <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>подстановочные знаки</link>.",
"logout_callback_url_description": "URL-адреса, которые твой клиент дает для выхода из системы. Поддерживаются <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>подстановочные знаки</link>.",
"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": "Авторизовать устройство",
@@ -474,29 +469,5 @@
"default_profile_picture": "Изображение профиля по умолчанию",
"light": "Светлая",
"dark": "Темная",
"system": "Системная",
"signup_token_user_groups_description": "Автоматически добавляй эти группы к пользователям, которые регистрируются с помощью этого токена.",
"allowed_oidc_clients": "Разрешенные клиенты OIDC",
"allowed_oidc_clients_description": "Выбери клиентов OIDC, к которым могут подключаться участники этой группы пользователей.",
"unrestrict_oidc_client": "Снять ограничения с {clientName}",
"confirm_unrestrict_oidc_client_description": "Ты уверен, что хочешь снять ограничения с клиента OIDC <b>{clientName}</b>? Это удалит все групповые назначения для этого клиента, и любой пользователь сможет войти в систему.",
"allowed_oidc_clients_updated_successfully": "Разрешенные клиенты OIDC обновились без проблем",
"yes": "Да",
"no": "Нет",
"restricted": "Ограниченный",
"scim_provisioning": "Настройка SCIM",
"scim_provisioning_description": "SCIM позволяет автоматически добавлять и удалять пользователей и группы из твоего клиента OIDC. Подробнее читай в <link href='https://pocket-id.org/docs/configuration/scim'>документации</link>.",
"scim_endpoint": "Конечная точка SCIM",
"scim_token": "Токен SCIM",
"last_successful_sync_at": "Последняя удачная синхронизация: {time}",
"scim_configuration_updated_successfully": "Настройки SCIM обновились без проблем.",
"scim_enabled_successfully": "SCIM включен, все работает.",
"scim_disabled_successfully": "SCIM отключен, все нормально.",
"disable_scim_provisioning": "Отключить настройку SCIM",
"disable_scim_provisioning_confirm_description": "Ты уверен, что хочешь отключить SCIM-провижининг для <b>{clientName}</b>? Это остановит все автоматические действия по предоставлению и отмене доступа пользователей и групп.",
"scim_sync_failed": "Синхронизация SCIM не получилась. Посмотри в журналах сервера, там будет больше инфо.",
"scim_sync_successful": "Синхронизация SCIM прошла без проблем.",
"save_and_sync": "Сохранить и синхронизировать",
"scim_save_changes_description": "Перед тем, как начать синхронизацию SCIM, нужно сохранить изменения. Хочешь сохранить сейчас?",
"scopes": "Области применения"
"system": "Системная"
}

View File

@@ -95,7 +95,7 @@
"settings": "Inställningar",
"update_pocket_id": "Uppdatera Pocket-ID",
"powered_by": "Drivs av",
"see_your_recent_account_activities": "Se dina kontoaktiviteter inom den konfigurerade lagringsperioden.",
"see_your_account_activities_from_the_last_3_months": "Se dina kontoaktiviteter från de senaste 3 månaderna.",
"time": "Tid",
"event": "Händelse",
"approximate_location": "Ungefärlig plats",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Är du säker på att du vill skapa en ny klienthemlighet? Den gamla kommer att ogiltigförklaras.",
"generate": "Generera",
"new_client_secret_created_successfully": "Ny klienthemlighet har skapats",
"allowed_user_groups_updated_successfully": "Tillåtna användargrupper har uppdaterats",
"oidc_client_name": "OIDC-klient {name}",
"client_id": "Client ID",
"client_secret": "Klienthemlighet",
"show_more_details": "Visa fler detaljer",
"allowed_user_groups": "Tillåtna användargrupper",
"allowed_user_groups_description": "Välj de användargrupper vars medlemmar får logga in på den här klienten.",
"allowed_user_groups_status_unrestricted_description": "Inga användargruppsbegränsningar tillämpas. Alla användare kan logga in på denna klient.",
"unrestrict": "Upphäva begränsning",
"restrict": "Begränsa",
"user_groups_restriction_updated_successfully": "Användargruppsbegränsningen har uppdaterats",
"allowed_user_groups_updated_successfully": "Tillåtna användargrupper har uppdaterats",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Lägg till användargrupper i denna klient för att begränsa åtkomsten till användare i dessa grupper. Om inga användargrupper väljs får alla användare åtkomst till denna klient.",
"favicon": "Favicon",
"light_mode_logo": "Logotyp för ljust läge",
"dark_mode_logo": "Logotyp för mörkt läge",
"email_logo": "E-postlogotyp",
"background_image": "Bakgrundsbild",
"language": "Språk",
"reset_profile_picture_question": "Återställ profilbild?",
@@ -332,7 +327,7 @@
"all_clients": "Alla klienter",
"all_locations": "Alla platser",
"global_audit_log": "Global granskningslogg",
"see_all_recent_account_activities": "Visa alla användares kontoaktiviteter under den angivna lagringsperioden.",
"see_all_account_activities_from_the_last_3_months": "Se all användaraktivitet för de senaste 3 månaderna.",
"token_sign_in": "Token-inloggning",
"client_authorization": "Godkännande av klient",
"new_client_authorization": "Ny klientauktorisation",
@@ -354,8 +349,8 @@
"login_code_email_success": "Inloggningskoden har skickats till användaren.",
"send_email": "Skicka e-postmeddelande",
"show_code": "Visa kod",
"callback_url_description": "URL-adresser som tillhandahålls av din klient. Läggs till automatiskt om fältet lämnas tomt. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Jokertecken</link> stöds.",
"logout_callback_url_description": "URL-adresser som din klient tillhandahåller för utloggning. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Jokertecken</link> stöds.",
"callback_url_description": "URL-adresser som tillhandahålls av din klient. Läggs till automatiskt om fältet lämnas tomt. Jokertecken (*) stöds, men bör undvikas för bättre säkerhet.",
"logout_callback_url_description": "URL-adresser som din klient tillhandahåller för utloggning. Jokertecken (*) stöds, men bör undvikas för bättre säkerhet.",
"api_key_expiration": "API-nyckelns giltighetstid",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Skicka ett e-postmeddelande till användaren när deras API-nyckel håller på att upphöra.",
"authorize_device": "Godkänn enhet",
@@ -474,29 +469,5 @@
"default_profile_picture": "Standardprofilbild",
"light": "Ljus",
"dark": "Mörk",
"system": "System",
"signup_token_user_groups_description": "Tilldela automatiskt dessa grupper till användare som registrerar sig med denna token.",
"allowed_oidc_clients": "Tillåtna OIDC-klienter",
"allowed_oidc_clients_description": "Välj de OIDC-klienter som medlemmarna i denna användargrupp får logga in på.",
"unrestrict_oidc_client": "Upphäva begränsning {clientName}",
"confirm_unrestrict_oidc_client_description": "Är du säker på att du vill ta bort begränsningarna för OIDC-klienten <b>{clientName}</b>? Detta kommer att ta bort alla grupptilldelningar för denna klient och alla användare kommer att kunna logga in.",
"allowed_oidc_clients_updated_successfully": "Tillåtna OIDC-klienter uppdaterade framgångsrikt",
"yes": "Ja",
"no": "Nej",
"restricted": "Begränsad",
"scim_provisioning": "SCIM-provisionering",
"scim_provisioning_description": "SCIM-provisionering gör det möjligt att automatiskt tilldela och återta behörigheter för användare och grupper från din OIDC-klient. Läs mer i <link href='https://pocket-id.org/docs/configuration/scim'>dokumentationen</link>.",
"scim_endpoint": "SCIM-slutpunkt",
"scim_token": "SCIM-token",
"last_successful_sync_at": "Senaste lyckade synkronisering: {time}",
"scim_configuration_updated_successfully": "SCIM-konfigurationen har uppdaterats.",
"scim_enabled_successfully": "SCIM har aktiverats framgångsrikt.",
"scim_disabled_successfully": "SCIM har inaktiverats.",
"disable_scim_provisioning": "Inaktivera SCIM-provisionering",
"disable_scim_provisioning_confirm_description": "Är du säker på att du vill inaktivera SCIM-provisionering för <b>{clientName}</b>? Detta kommer att stoppa all automatisk provisionering och avprovisionering av användare och grupper.",
"scim_sync_failed": "SCIM-synkroniseringen misslyckades. Kontrollera serverloggarna för mer information.",
"scim_sync_successful": "SCIM-synkroniseringen har slutförts.",
"save_and_sync": "Spara och synkronisera",
"scim_save_changes_description": "Du måste spara ändringarna innan du startar en SCIM-synkronisering. Vill du spara nu?",
"scopes": "Omfattning"
"system": "System"
}

View File

@@ -95,7 +95,7 @@
"settings": "Ayarlar",
"update_pocket_id": "Pocket ID'yi Güncelle",
"powered_by": "Destekleyen",
"see_your_recent_account_activities": "Yapılandırılan saklama süresi içinde hesap hareketlerinizi görüntüleyin.",
"see_your_account_activities_from_the_last_3_months": "Son 3 aydaki hesap aktivitelerinizi görüntüleyin.",
"time": "Zaman",
"event": "Olay",
"approximate_location": "Yaklaşık Konum",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Yeni bir istemci sırrı oluşturmak istediğinizden emin misiniz? Eskisi geçersiz kılınacaktır.",
"generate": "Üret",
"new_client_secret_created_successfully": "Yeni istemci sırrı başarıyla oluşturuldu",
"allowed_user_groups_updated_successfully": "İzin verilen kullanıcı grupları başarıyla güncellendi",
"oidc_client_name": "OIDC İstemcisi {name}",
"client_id": "İstemci kimliği",
"client_secret": "İstemci sırrı",
"show_more_details": "Daha fazla detay göster",
"allowed_user_groups": "İzin Verilen Kullanıcı Grupları",
"allowed_user_groups_description": "Bu istemciye oturum açmasına izin verilen kullanıcı gruplarını seçin.",
"allowed_user_groups_status_unrestricted_description": "Kullanıcı grubu kısıtlaması uygulanmaz. Herhangi bir kullanıcı bu istemciye oturum açabilir.",
"unrestrict": "Kısıtlama kaldır",
"restrict": "Kısıtla",
"user_groups_restriction_updated_successfully": "Kullanıcı grupları kısıtlaması başarıyla güncellendi",
"allowed_user_groups_updated_successfully": "İzin verilen kullanıcı grupları başarıyla güncellendi",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Bu istemciye kullanıcı grupları ekleyerek bu gruplardaki kullanıcılara erişimi kısıtlayın. Hiçbir kullanıcı grubu seçilmezse, tüm kullanıcılar bu istemciye erişebilecektir.",
"favicon": "Favicon",
"light_mode_logo": "Açık Mod Logo",
"dark_mode_logo": "Karanlık Mod Logo",
"email_logo": "E-posta Logosu",
"background_image": "Arkaplan Resmi",
"language": "Dil",
"reset_profile_picture_question": "Profil resmini sıfırlamak istiyor musunuz?",
@@ -332,7 +327,7 @@
"all_clients": "Tüm İstemciler",
"all_locations": "Tüm Konumlar",
"global_audit_log": "Genel Denetim Kaydı",
"see_all_recent_account_activities": "Belirlenen saklama süresi boyunca tüm kullanıcıların hesap etkinliklerini görüntüleyin.",
"see_all_account_activities_from_the_last_3_months": "Son 3 ay içindeki tüm kullanıcı etkinliklerini görüntüle.",
"token_sign_in": "Token ile Giriş",
"client_authorization": "İstemci Yetkilendirmesi",
"new_client_authorization": "Yeni İstemci Yetkilendirmesi",
@@ -354,8 +349,8 @@
"login_code_email_success": "Giriş kodu kullanıcıya gönderildi.",
"send_email": "E-posta Gönder",
"show_code": "Kodu Göster",
"callback_url_description": "Müşteriniz tarafından sağlanan URL'ler. Boş bırakılırsa otomatik olarak eklenir. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Joker karakterler</link> desteklenir.",
"logout_callback_url_description": "Müşteriniz tarafından oturumu kapatmak için sağlanan URL'ler. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Joker karakterler</link> desteklenir.",
"callback_url_description": "URL'ler, istemciniz tarafından sağlanır. Boş bırakılırsa otomatik olarak eklenecektir. Wildcard (*) karakterleri desteklenmektedir, ancak daha iyi güvenlik için kaçınılması önerilir.",
"logout_callback_url_description": "Çıkış URL'leri, istemciniz tarafından sağlanır. Boş bırakılırsa otomatik olarak eklenecektir. Wildcard (*) karakterleri desteklenmektedir, ancak daha iyi güvenlik için kaçınılması önerilir.",
"api_key_expiration": "API Anahtarının Geçerlilik Süresi",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API anahtarları sona ermek üzere olduğunda kullanıcıya bir e-posta gönderin.",
"authorize_device": "Cihazı Yetkilendir",
@@ -472,31 +467,7 @@
"reauthentication": "Yeniden Kimlik Doğrulama",
"clear_filters": "Filtreleri Temizle",
"default_profile_picture": "Varsayılan Profil Resmi",
"light": "Aydınlık",
"light": "ık",
"dark": "Karanlık",
"system": "Sistem",
"signup_token_user_groups_description": "Bu token kullanarak kaydolan kullanıcılara bu grupları otomatik olarak atayın.",
"allowed_oidc_clients": "İzin Verilen OIDC İstemcileri",
"allowed_oidc_clients_description": "Bu kullanıcı grubunun üyelerinin oturum açmasına izin verilen OIDC istemcilerini seçin.",
"unrestrict_oidc_client": "Kısıtlama kaldır {clientName}",
"confirm_unrestrict_oidc_client_description": "OIDC istemcisinin kısıtlamasını kaldırmak istediğinizden emin misiniz <b>{clientName}</b>? Bu, bu istemci için tüm grup atamalarını kaldıracak ve herhangi bir kullanıcı oturum açabilecektir.",
"allowed_oidc_clients_updated_successfully": "İzin verilen OIDC istemcileri başarıyla güncellendi",
"yes": "Evet",
"no": "Hayır",
"restricted": "Kısıtlı",
"scim_provisioning": "SCIM Sağlama",
"scim_provisioning_description": "SCIM provizyonu, OIDC istemcinizden kullanıcıları ve grupları otomatik olarak provizyonlamanıza ve provizyonunu kaldırmanıza olanak tanır. Daha fazla bilgi için <link href='https://pocket-id.org/docs/configuration/scim'>belgelere</link> bakın.",
"scim_endpoint": "SCIM Uç Noktası",
"scim_token": "SCIM Token",
"last_successful_sync_at": "Son başarılı senkronizasyon: {time}",
"scim_configuration_updated_successfully": "SCIM yapılandırması başarıyla güncellendi.",
"scim_enabled_successfully": "SCIM başarıyla etkinleştirildi.",
"scim_disabled_successfully": "SCIM başarıyla devre dışı bırakıldı.",
"disable_scim_provisioning": "SCIM Sağlamayı Devre Dışı Bırak",
"disable_scim_provisioning_confirm_description": "SCIM provizyonunu devre dışı bırakmak istediğinizden emin misiniz <b>{clientName}</b>Bu, tüm otomatik kullanıcı ve grup provizyonunu ve provizyonun kaldırılmasını durduracaktır.",
"scim_sync_failed": "SCIM senkronizasyonu başarısız oldu. Daha fazla bilgi için sunucu günlüklerini kontrol edin.",
"scim_sync_successful": "SCIM senkronizasyonu başarıyla tamamlandı.",
"save_and_sync": "Kaydet ve Senkronize Et",
"scim_save_changes_description": "SCIM senkronizasyonunu başlatmadan önce değişiklikleri kaydetmeniz gerekir. Şimdi kaydetmek ister misiniz?",
"scopes": "Kapsamlar"
"system": "Sistem"
}

View File

@@ -95,7 +95,7 @@
"settings": "Налаштування",
"update_pocket_id": "Оновити Pocket ID",
"powered_by": "Працює на базі",
"see_your_recent_account_activities": "Перегляньте активність вашого облікового запису протягом налаштованого періоду зберігання.",
"see_your_account_activities_from_the_last_3_months": "Перегляньте активність вашого облікового запису за останні 3 місяці.",
"time": "Час",
"event": "Подія",
"approximate_location": "Приблизне місцеперебування",
@@ -301,21 +301,16 @@
"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": "Дозволені групи користувачів",
"allowed_user_groups_description": "Виберіть групи користувачів, члени яких мають право входити в цей клієнт.",
"allowed_user_groups_status_unrestricted_description": "Обмеження для груп користувачів не застосовуються. Будь-який користувач може увійти в цей клієнт.",
"unrestrict": "Без обмежень",
"restrict": "Обмежити",
"user_groups_restriction_updated_successfully": "Обмеження груп користувачів успішно оновлено",
"allowed_user_groups_updated_successfully": "Дозволені групи користувачів успішно оновлено",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Додайте групи користувачів до цього клієнта, щоб обмежити доступ користувачам лише з цих груп. Якщо жодна група користувачів не обрана, усі користувачі матимуть доступ до цього клієнта.",
"favicon": "Фавікон",
"light_mode_logo": "Логотип світлого режиму",
"dark_mode_logo": "Логотип темного режиму",
"email_logo": "Логотип електронної пошти",
"background_image": "Фонове зображення",
"language": "Мова",
"reset_profile_picture_question": "Скинути зображення профілю?",
@@ -332,7 +327,7 @@
"all_clients": "Усі клієнти",
"all_locations": "Усі місця розташування",
"global_audit_log": "Глобальний журнал авдиту",
"see_all_recent_account_activities": "Перегляньте активність облікових записів усіх користувачів протягом встановленого періоду зберігання.",
"see_all_account_activities_from_the_last_3_months": "Переглянути всю активність користувача за останні 3 місяці.",
"token_sign_in": "Вхід за допомогою токена",
"client_authorization": "Авторизація клієнта",
"new_client_authorization": "Нова авторизація клієнта",
@@ -354,8 +349,8 @@
"login_code_email_success": "Код для входу було надіслано користувачеві.",
"send_email": "Надіслати електронного листа",
"show_code": "Показати код",
"callback_url_description": "URL-адреси, надані вашим клієнтом. Будуть додані автоматично, якщо залишити поле порожнім. Підтримуються <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>символи-замінники</link>.",
"logout_callback_url_description": "URL-адреси, надані вашим клієнтом для виходу з системи. Підтримуються <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>символи-замінники</link>.",
"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": "Авторизувати пристрій",
@@ -474,29 +469,5 @@
"default_profile_picture": "Стандартне зображення профілю",
"light": "Світло",
"dark": "Темний",
"system": "Система",
"signup_token_user_groups_description": "Автоматично призначайте ці групи користувачам, які реєструються за допомогою цього токена.",
"allowed_oidc_clients": "Дозволені клієнти OIDC",
"allowed_oidc_clients_description": "Виберіть клієнти OIDC, до яких члени цієї групи користувачів мають право входити.",
"unrestrict_oidc_client": "Без обмежень {clientName}",
"confirm_unrestrict_oidc_client_description": "Ви впевнені, що хочете зняти обмеження з клієнта OIDC <b>{clientName}</b>? Це призведе до видалення всіх групових призначень для цього клієнта, і будь-який користувач зможе увійти в систему.",
"allowed_oidc_clients_updated_successfully": "Дозволені клієнти OIDC успішно оновлені",
"yes": "Так",
"no": "Ні",
"restricted": "Обмежений",
"scim_provisioning": "Надання SCIM",
"scim_provisioning_description": "SCIM-провізінінг дозволяє автоматично надавати та скасовувати доступ користувачам і групам з вашого клієнта OIDC. Дізнайтеся більше в <link href='https://pocket-id.org/docs/configuration/scim'>документації</link>.",
"scim_endpoint": "Кінцева точка SCIM",
"scim_token": "Токен SCIM",
"last_successful_sync_at": "Остання успішна синхронізація: {time}",
"scim_configuration_updated_successfully": "Конфігурація SCIM успішно оновлена.",
"scim_enabled_successfully": "SCIM успішно увімкнено.",
"scim_disabled_successfully": "SCIM успішно вимкнено.",
"disable_scim_provisioning": "Вимкнути надання SCIM",
"disable_scim_provisioning_confirm_description": "Ви впевнені, що хочете вимкнути надання доступу SCIM для <b>{clientName}</b>? Це зупинить всі автоматичні процеси надання та скасування доступу для користувачів і груп.",
"scim_sync_failed": "Синхронізація SCIM не вдалася. Перевірте журнали сервера для отримання додаткової інформації.",
"scim_sync_successful": "Синхронізація SCIM успішно завершена.",
"save_and_sync": "Зберегти та синхронізувати",
"scim_save_changes_description": "Перед початком синхронізації SCIM необхідно зберегти зміни. Чи хочете ви зберегти зараз?",
"scopes": "Області застосування"
"system": "Система"
}

View File

@@ -95,7 +95,7 @@
"settings": "Cài đặt",
"update_pocket_id": "Cập nhật Pocket ID",
"powered_by": "Được cung cấp bởi",
"see_your_recent_account_activities": "Xem các hoạt động tài khoản của bạn trong khoảng thời gian lưu trữ đã được cấu hình.",
"see_your_account_activities_from_the_last_3_months": "Xem các hoạt động tài khoản của bạn trong 3 tháng qua.",
"time": "Thời gian",
"event": "Sự kiện",
"approximate_location": "Vị Trí Ước Tính",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Bạn có chắc chắn muốn tạo một client secret? Client secret cũ sẽ bị vô hiệu hóa.",
"generate": "Tạo ra",
"new_client_secret_created_successfully": "Client secret mới đã được tạo thành công",
"allowed_user_groups_updated_successfully": "Các nhóm người dùng được phép đã được cập nhật thành công",
"oidc_client_name": "OIDC Client {name}",
"client_id": "Client ID",
"client_secret": "Client secret",
"show_more_details": "Hiển thị thêm thông tin",
"allowed_user_groups": "Nhóm người dùng được phép",
"allowed_user_groups_description": "Chọn các nhóm người dùng mà thành viên của họ được phép đăng nhập vào ứng dng này.",
"allowed_user_groups_status_unrestricted_description": "Không áp dụng bất kỳ hạn chế nào đối với nhóm người dùng. Bất kỳ người dùng nào cũng có thể đăng nhập vào ứng dụng này.",
"unrestrict": "Không giới hạn",
"restrict": "Hạn chế",
"user_groups_restriction_updated_successfully": "Hạn chế nhóm người dùng đã được cập nhật thành công.",
"allowed_user_groups_updated_successfully": "Các nhóm người dùng được phép đã được cập nhật thành công",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Thêm nhóm người dùng vào khách hàng này để hạn chế quyền truy cập cho người dùng trong các nhóm này. Nếu không chọn nhóm người dùng nào, tất cả người dùng sẽ có quyền truy cập vào client này.",
"favicon": "Biểu tượng favicon",
"light_mode_logo": "Logo cho Light Mode",
"dark_mode_logo": "Logo cho Dark Mode",
"email_logo": "Logo email",
"background_image": "Ảnh nền",
"language": "Ngôn ngữ",
"reset_profile_picture_question": "Đặt lại ảnh đại diện?",
@@ -332,7 +327,7 @@
"all_clients": "Tất cả clients",
"all_locations": "Tất cả địa điểm",
"global_audit_log": "Nhật ký kiểm tra toàn cầu",
"see_all_recent_account_activities": "Xem các hoạt động tài khoản của tất cả người dùng trong khoảng thời gian lưu trữ đã thiết lập.",
"see_all_account_activities_from_the_last_3_months": "Xem tất cả hoạt động của người dùng trong 3 tháng qua.",
"token_sign_in": "Đăng nhập bằng token",
"client_authorization": "Xác thực client",
"new_client_authorization": "Xác thực client mới",
@@ -354,8 +349,8 @@
"login_code_email_success": "Mã đăng nhập đã được gửi đến người dùng.",
"send_email": "Gửi Email",
"show_code": "Hiện Thị Mã",
"callback_url_description": "URL(s) do khách hàng của bạn cung cấp. Sẽ được tự động thêm vào nếu để trống. Hỗ trợ <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>ký tự đại diện</link>.",
"logout_callback_url_description": "Các URL do khách hàng của bạn cung cấp để đăng xuất. Hỗ trợ <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>ký tự đại diện</link>.",
"callback_url_description": "URL do client của bạn cung cấp. Sẽ được tự động thêm nếu để trống. Hỗ trợ ký tự đại diện (*), nhưng nên tránh sử dụng để đảm bảo an toàn.",
"logout_callback_url_description": "URL do client của bạn cung cấp để đăng xuất. Ký tự đại diện (*) được hỗ trợ, nhưng nên tránh sử dụng để đảm bảo an toàn.",
"api_key_expiration": "Hạn sử dụng API Key",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Gửi email cho người dùng khi API key của họ sắp hết hạn.",
"authorize_device": "Thiết Bị Được Cho Phép",
@@ -474,29 +469,5 @@
"default_profile_picture": "Ảnh đại diện mặc định",
"light": "Ánh sáng",
"dark": "Tối",
"system": "Hệ thống",
"signup_token_user_groups_description": "Tự động gán các nhóm này cho người dùng đăng ký bằng token này.",
"allowed_oidc_clients": "Các khách hàng OIDC được phép",
"allowed_oidc_clients_description": "Chọn các khách hàng OIDC mà các thành viên của nhóm người dùng này được phép đăng nhập vào.",
"unrestrict_oidc_client": "Không giới hạn {clientName}",
"confirm_unrestrict_oidc_client_description": "Bạn có chắc chắn muốn gỡ bỏ hạn chế cho khách hàng OIDC <b>{clientName}</b>? Điều này sẽ xóa tất cả các gán nhóm cho khách hàng này và bất kỳ người dùng nào cũng có thể đăng nhập.",
"allowed_oidc_clients_updated_successfully": "Các khách hàng OIDC được phép đã được cập nhật thành công.",
"yes": "Có",
"no": "Không",
"restricted": "Hạn chế",
"scim_provisioning": "Cấu hình SCIM",
"scim_provisioning_description": "SCIM provisioning cho phép bạn tự động cấp quyền và thu hồi quyền truy cập cho người dùng và nhóm từ ứng dụng khách OIDC của bạn. Tìm hiểu thêm trong <link href='https://pocket-id.org/docs/configuration/scim'>tài liệu.</link>",
"scim_endpoint": "Điểm cuối SCIM",
"scim_token": "SCIM Token",
"last_successful_sync_at": "Lần đồng bộ hóa thành công gần nhất: {time}",
"scim_configuration_updated_successfully": "Cấu hình SCIM đã được cập nhật thành công.",
"scim_enabled_successfully": "SCIM đã được kích hoạt thành công.",
"scim_disabled_successfully": "SCIM đã được vô hiệu hóa thành công.",
"disable_scim_provisioning": "Vô hiệu hóa việc cấp phát SCIM",
"disable_scim_provisioning_confirm_description": "Bạn có chắc chắn muốn vô hiệu hóa việc cấp phát SCIM cho <b>{clientName}</b>? Điều này sẽ ngừng tất cả các hoạt động cấp phép và thu hồi cấp phép tự động cho người dùng và nhóm.",
"scim_sync_failed": "Quá trình đồng bộ hóa SCIM đã thất bại. Vui lòng kiểm tra nhật ký máy chủ để biết thêm thông tin.",
"scim_sync_successful": "Quá trình đồng bộ hóa SCIM đã hoàn tất thành công.",
"save_and_sync": "Lưu và Đồng bộ hóa",
"scim_save_changes_description": "Bạn phải lưu các thay đổi trước khi bắt đầu đồng bộ hóa SCIM. Bạn có muốn lưu ngay bây giờ không?",
"scopes": "Phạm vi"
"system": "Hệ thống"
}

View File

@@ -86,7 +86,7 @@
"enter_the_code_you_received_to_sign_in": "输入您收到的登录码以登录。",
"code": "代码",
"invalid_redirect_url": "无效的重定向 URL",
"audit_log": "审计日志",
"audit_log": "日志",
"users": "用户",
"user_groups": "用户组",
"oidc_clients": "OIDC 客户端",
@@ -95,7 +95,7 @@
"settings": "设置",
"update_pocket_id": "更新 Pocket ID",
"powered_by": "",
"see_your_recent_account_activities": "查看您账户在配置的保留期内的活动记录。",
"see_your_account_activities_from_the_last_3_months": "查看过去 3 个月的账户活动。",
"time": "时间",
"event": "事件",
"approximate_location": "大致位置",
@@ -301,21 +301,16 @@
"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": "允许的用户组",
"allowed_user_groups_description": "选择允许其成员登录此客户端的用户组。",
"allowed_user_groups_status_unrestricted_description": "未应用任何用户组限制。任何用户均可登录此客户端。",
"unrestrict": "解除限制",
"restrict": "限制",
"user_groups_restriction_updated_successfully": "用户组限制已成功更新",
"allowed_user_groups_updated_successfully": "已成功更新允许的用户组",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "将用户组添加到此客户端以限制访问,仅允许这些组中的用户。如果未选择用户组,所有用户都将有权访问此客户端。",
"favicon": "网站图标",
"light_mode_logo": "浅色模式 Logo",
"dark_mode_logo": "深色模式 Logo",
"email_logo": "电子邮件徽标",
"background_image": "背景图片",
"language": "语言",
"reset_profile_picture_question": "重置头像?",
@@ -332,7 +327,7 @@
"all_clients": "所有客户端",
"all_locations": "所有地方",
"global_audit_log": "全局日志",
"see_all_recent_account_activities": "查看所有用户在设定保留期内的账户活动。",
"see_all_account_activities_from_the_last_3_months": "查看过去 3 个月的所有用户活动。",
"token_sign_in": "Token 登录",
"client_authorization": "客户端授权",
"new_client_authorization": "首次客户端授权",
@@ -354,8 +349,8 @@
"login_code_email_success": "登录代码已发送给用户。",
"send_email": "发送电子邮件",
"show_code": "显示登录码",
"callback_url_description": "由您的客户提供的网址。若留空,将自动添加。支持<link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>通配符</link>。",
"logout_callback_url_description": "由您客户端提供的注销URL。支持<link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>通配符</link>。",
"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": "授权设备",
@@ -472,31 +467,7 @@
"reauthentication": "重新认证",
"clear_filters": "清除筛选条件",
"default_profile_picture": "默认个人资料图片",
"light": "浅色",
"dark": "深色",
"system": "系统",
"signup_token_user_groups_description": "自动将这些组分配给使用此令牌注册的用户。",
"allowed_oidc_clients": "允许的 OIDC 客户端",
"allowed_oidc_clients_description": "选择允许此用户组成员登录的 OIDC 客户端。",
"unrestrict_oidc_client": "解除限制 {clientName}",
"confirm_unrestrict_oidc_client_description": "您确定要解除对该 OIDC 客户端的限制吗? <b>{clientName}</b>?此操作将移除此客户端的所有组分配,任何用户均可登录。",
"allowed_oidc_clients_updated_successfully": "允许的 OIDC 客户端已成功更新",
"yes": "是的",
"no": "不",
"restricted": "限制",
"scim_provisioning": "SCIM 配置",
"scim_provisioning_description": "SCIM 配置功能可让您自动为 OIDC 客户端创建和删除用户及组。更多详情请参<link href='https://pocket-id.org/docs/configuration/scim'>阅文档</link>。",
"scim_endpoint": "SCIM 端点",
"scim_token": "SCIM 令牌",
"last_successful_sync_at": "上次成功同步: {time}",
"scim_configuration_updated_successfully": "SCIM 配置更新成功。",
"scim_enabled_successfully": "SCIM已成功启用。",
"scim_disabled_successfully": "SCIM已成功禁用。",
"disable_scim_provisioning": "禁用 SCIM 配置",
"disable_scim_provisioning_confirm_description": "您确定要禁用 SCIM 配置吗? <b>{clientName}</b>?此操作将停止所有用户和组的自动配置与撤销配置。",
"scim_sync_failed": "SCIM 同步失败。请检查服务器日志以获取更多信息。",
"scim_sync_successful": "SCIM 同步已成功完成。",
"save_and_sync": "保存并同步",
"scim_save_changes_description": "在开始 SCIM 同步之前,您必须先保存更改。是否现在保存?",
"scopes": "Scopes"
"light": "",
"dark": "黑暗",
"system": "系统"
}

View File

@@ -95,7 +95,7 @@
"settings": "設定",
"update_pocket_id": "更新 Pocket ID",
"powered_by": "技術支援",
"see_your_recent_account_activities": "查看您在設定的保留期間內的帳活動。",
"see_your_account_activities_from_the_last_3_months": "查看您過去 3 個月的帳活動。",
"time": "時間",
"event": "事件",
"approximate_location": "概略位置",
@@ -301,21 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "確定要建立新的 client secret 嗎?舊的將會失效。",
"generate": "產生",
"new_client_secret_created_successfully": "新的 client secret 建立成功",
"allowed_user_groups_updated_successfully": "允許的使用者群組已成功更新",
"oidc_client_name": "OIDC 客戶端 {name}",
"client_id": "Client ID",
"client_secret": "Client secret",
"show_more_details": "顯示更多資訊",
"allowed_user_groups": "允許的使用者群組",
"allowed_user_groups_description": "選擇允許其成員登入此客戶端的用戶群組。",
"allowed_user_groups_status_unrestricted_description": "未套用任何使用者群組限制。任何使用者皆可登入此客戶端。",
"unrestrict": "解除限制",
"restrict": "限制",
"user_groups_restriction_updated_successfully": "使用者群組限制已成功更新",
"allowed_user_groups_updated_successfully": "允許的使用者群組已成功更新",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "將使用者群組新增至此客戶端,以限制只有這些群組中的使用者可以存取。若未選擇任何群組,所有使用者都將能存取此客戶端。",
"favicon": "Favicon",
"light_mode_logo": "亮色模式標誌",
"dark_mode_logo": "暗色模式標誌",
"email_logo": "電子郵件標誌",
"background_image": "背景圖片",
"language": "語言",
"reset_profile_picture_question": "重設個人資料圖片?",
@@ -332,7 +327,7 @@
"all_clients": "所有客戶端",
"all_locations": "所有地點",
"global_audit_log": "全域稽核日誌",
"see_all_recent_account_activities": "檢視所有使用者在設定的保留期間內的帳戶活動。",
"see_all_account_activities_from_the_last_3_months": "查看過去 3 個月的所有使用者活動。",
"token_sign_in": "Token 登入",
"client_authorization": "客戶端授權",
"new_client_authorization": "新客戶端授權",
@@ -354,8 +349,8 @@
"login_code_email_success": "登入代碼已傳送給使用者。",
"send_email": "發送郵件",
"show_code": "顯示代碼",
"callback_url_description": "由您的客戶提供的網址。若留空,將自動添加。<link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>支援萬用字元</link>。",
"logout_callback_url_description": "由您的客戶端提供的登出網址。<link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>支援萬用字元</link>。",
"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": "授權裝置",
@@ -474,29 +469,5 @@
"default_profile_picture": "預設個人資料照片",
"light": "光",
"dark": "黑暗",
"system": "系統",
"signup_token_user_groups_description": "自動將這些群組指派給使用此代幣註冊的用戶。",
"allowed_oidc_clients": "允許的 OIDC 客戶端",
"allowed_oidc_clients_description": "選擇允許此使用者群組成員登入的 OIDC 客戶端。",
"unrestrict_oidc_client": "解除限制 {clientName}",
"confirm_unrestrict_oidc_client_description": "您確定要解除 OIDC 客戶端的限制嗎? <b>{clientName}</b>?此操作將移除此客戶端的所有群組指派,任何使用者皆可登入。",
"allowed_oidc_clients_updated_successfully": "已允許的 OIDC 客戶端更新成功",
"yes": "是的",
"no": "不",
"restricted": "限制",
"scim_provisioning": "SCIM 配置",
"scim_provisioning_description": "SCIM 配置功能可讓您從 OIDC 客戶端自動配置及取消配置使用者與群組。更多資訊請參<link href='https://pocket-id.org/docs/configuration/scim'>閱文件說明</link>。",
"scim_endpoint": "SCIM 端點",
"scim_token": "SCIM 憑證",
"last_successful_sync_at": "最後一次成功同步: {time}",
"scim_configuration_updated_successfully": "SCIM 設定已成功更新。",
"scim_enabled_successfully": "SCIM 已成功啟用。",
"scim_disabled_successfully": "SCIM 已成功停用。",
"disable_scim_provisioning": "停用 SCIM 配置",
"disable_scim_provisioning_confirm_description": "您確定要停用 SCIM 配置功能嗎? <b>{clientName}</b>?此操作將停止所有使用者與群組的自動配置及撤銷配置。",
"scim_sync_failed": "SCIM 同步失敗。請檢查伺服器日誌以獲取更多資訊。",
"scim_sync_successful": "SCIM 同步已成功完成。",
"save_and_sync": "儲存與同步",
"scim_save_changes_description": "您必須在開始 SCIM 同步前儲存變更。現在要儲存嗎?",
"scopes": "範圍"
"system": "系統"
}

View File

@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "2.0.0",
"version": "1.16.0",
"private": true,
"type": "module",
"scripts": {
@@ -15,49 +15,49 @@
},
"dependencies": {
"@simplewebauthn/browser": "^13.2.2",
"@tailwindcss/vite": "^4.1.18",
"@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"jose": "^6.1.3",
"jose": "^6.1.2",
"qrcode": "^1.5.4",
"runed": "^0.37.1",
"sveltekit-superforms": "^2.29.1",
"runed": "^0.37.0",
"sveltekit-superforms": "^2.28.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.4"
"zod": "^4.1.13"
},
"devDependencies": {
"@inlang/paraglide-js": "^2.7.1",
"@inlang/paraglide-js": "^2.5.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@internationalized/date": "^3.10.1",
"@lucide/svelte": "^0.559.0",
"@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.555.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.49.2",
"@sveltejs/kit": "^2.49.0",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.1",
"@types/node": "^24.10.4",
"@types/node": "^24.10.1",
"@types/qrcode": "^1.5.6",
"bits-ui": "^2.14.4",
"eslint": "^9.39.2",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.13.1",
"eslint-plugin-svelte": "^3.13.0",
"formsnap": "^2.0.1",
"globals": "^16.5.0",
"mode-watcher": "^1.1.0",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"rollup": "^4.54.0",
"svelte": "^5.46.1",
"svelte-check": "^4.3.5",
"svelte-sonner": "^1.0.7",
"prettier": "^3.7.3",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.7.1",
"rollup": "^4.53.3",
"svelte": "^5.45.2",
"svelte-check": "^4.3.4",
"svelte-sonner": "^1.0.6",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.18",
"tailwindcss": "^4.1.17",
"tslib": "^2.8.1",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.51.0",
"vite": "^7.3.0"
"typescript-eslint": "^8.48.0",
"vite": "^7.2.4"
}
}

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