mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-17 02:09:52 +00:00
Compare commits
11 Commits
v2.0.2
...
oidc-scope
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b11860b864 | ||
|
|
c4bd20a90d | ||
|
|
900f8fe240 | ||
|
|
1da694f8ad | ||
|
|
e06538a101 | ||
|
|
39c1f93756 | ||
|
|
e306d6eb58 | ||
|
|
46793fe68a | ||
|
|
e22822890f | ||
|
|
2694d79add | ||
|
|
98cf1f66c3 |
2
.github/workflows/backend-linter.yml
vendored
2
.github/workflows/backend-linter.yml
vendored
@@ -2,7 +2,7 @@ name: Run Backend Linter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, breaking/**]
|
||||
paths:
|
||||
- "backend/**"
|
||||
pull_request:
|
||||
|
||||
24
.github/workflows/e2e-tests.yml
vendored
24
.github/workflows/e2e-tests.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/svelte-check.yml
vendored
2
.github/workflows/svelte-check.yml
vendored
@@ -2,7 +2,7 @@ name: Svelte Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, breaking/**]
|
||||
paths:
|
||||
- "frontend/src/**"
|
||||
- ".github/svelte-check-matcher.json"
|
||||
|
||||
2
.github/workflows/unit-tests.yml
vendored
2
.github/workflows/unit-tests.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, breaking/**]
|
||||
paths:
|
||||
- "backend/**"
|
||||
pull_request:
|
||||
|
||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -1,74 +1,3 @@
|
||||
## v2.0.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- migration fails if users exist with no email address ([2f651ad](https://github.com/pocket-id/pocket-id/commit/2f651adf3b4e8d689461da2083c3afcb1eb1d477) by @stonith404)
|
||||
- allow version downgrade database is dirty ([ba00f40](https://github.com/pocket-id/pocket-id/commit/ba00f40bd4b06f31d251599fcb1db63e902a6987) by @stonith404)
|
||||
- localhost callback URLs with port don't match correctly ([7c34501](https://github.com/pocket-id/pocket-id/commit/7c345010556f11a593948b2a1ae558b7a8003696) by @stonith404)
|
||||
|
||||
### Other
|
||||
|
||||
- add no-op migration to postgres ([a24b2af](https://github.com/pocket-id/pocket-id/commit/a24b2afb7b8165bed05976058a8ae797adc245df) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.0.1...v2.0.2
|
||||
|
||||
## v2.0.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- admins imported from LDAP lose admin privileges ([2cce200](https://github.com/pocket-id/pocket-id/commit/2cce2008928081b5e0f0e6bcbc3f43816f082de9) by @stonith404)
|
||||
- restore old input input field size ([2341da9](https://github.com/pocket-id/pocket-id/commit/2341da99e9716686cf28dd0680d751ae9da0fadc) by @stonith404)
|
||||
|
||||
### Other
|
||||
|
||||
- bump image tag to `v2` ([cd2e9f3](https://github.com/pocket-id/pocket-id/commit/cd2e9f3a2ad753815ef8da998f9b54853d953a2a) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.0.0...v2.0.1
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
149
backend/go.mod
149
backend/go.mod
@@ -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
|
||||
)
|
||||
|
||||
311
backend/go.sum
311
backend/go.sum
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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;"`
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,206 +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.
|
||||
loopbackCallbackURLWithoutPort := ""
|
||||
u, _ := url.Parse(inputCallbackURL)
|
||||
|
||||
if u != nil && u.Scheme == "http" {
|
||||
host := u.Hostname()
|
||||
ip := net.ParseIP(host)
|
||||
if host == "localhost" || (ip != nil && ip.IsLoopback()) {
|
||||
u.Host = host
|
||||
loopbackCallbackURLWithoutPort = u.String()
|
||||
}
|
||||
}
|
||||
|
||||
for _, pattern := range urls {
|
||||
// Try the original callback first
|
||||
matches, err := matchCallbackURL(pattern, inputCallbackURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if matches {
|
||||
return inputCallbackURL, nil
|
||||
}
|
||||
|
||||
// If we have a loopback variant, try that too
|
||||
if loopbackCallbackURLWithoutPort != "" {
|
||||
matches, err = matchCallbackURL(pattern, loopbackCallbackURLWithoutPort)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if matches {
|
||||
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
|
||||
}
|
||||
@@ -1,791 +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=*¶m=*",
|
||||
"https://example.com/callback?param=value1¶m=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 same port - exact match",
|
||||
urls: []string{"http://127.0.0.1:8080/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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,7 @@ func MigrateDatabase(sqlDb *sql.DB) error {
|
||||
return migrateDatabaseFromGitHub(sqlDb, requiredVersion)
|
||||
}
|
||||
|
||||
err = m.Migrate(requiredVersion)
|
||||
if err != nil {
|
||||
if errors.Is(err, migrate.ErrNoChange) {
|
||||
return nil
|
||||
}
|
||||
if errors.As(err, &migrate.ErrDirty{}) {
|
||||
return fmt.Errorf("database migration failed. Please create an issue on GitHub and temporarely downgrade to the previous version: %w", err)
|
||||
}
|
||||
if err := m.Migrate(requiredVersion); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("failed to apply embedded migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -105,7 +98,7 @@ func migrateDatabaseFromGitHub(sqlDb *sql.DB, version uint) error {
|
||||
return fmt.Errorf("failed to create GitHub migration instance: %w", err)
|
||||
}
|
||||
|
||||
if err := m.Force(int(version)); err != nil && !errors.Is(err, migrate.ErrNoChange) { //nolint:gosec
|
||||
if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("failed to apply GitHub migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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 |
@@ -0,0 +1 @@
|
||||
-- No-op in Postgres
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op in Postgres
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE oidc_apis;
|
||||
@@ -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);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE one_time_access_tokens DROP COLUMN device_token;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE one_time_access_tokens ADD COLUMN device_token VARCHAR(16);
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE signup_tokens_user_groups;
|
||||
@@ -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
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
-- This migration is part of v2
|
||||
|
||||
-- No-op in Postgres
|
||||
@@ -1,3 +0,0 @@
|
||||
-- This migration is part of v2
|
||||
|
||||
-- No-op in Postgres
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN is_group_restricted;
|
||||
@@ -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
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
DROP TABLE scim_service_providers;
|
||||
ALTER TABLE users DROP COLUMN updated_at;
|
||||
ALTER TABLE user_groups DROP COLUMN updated_at;
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
UPDATE app_config_variables SET value = 'ldapAttributeAdminGroup' WHERE value = 'ldapAdminGroupName';
|
||||
@@ -1,8 +0,0 @@
|
||||
UPDATE app_config_variables
|
||||
SET key = 'ldapAdminGroupName'
|
||||
WHERE key = 'ldapAttributeAdminGroup'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM app_config_variables
|
||||
WHERE key = 'ldapAdminGroupName'
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
-- No-op on Postgres
|
||||
@@ -1 +0,0 @@
|
||||
-- No-op on Postgres
|
||||
@@ -1,5 +1,3 @@
|
||||
-- This migration is part of v2
|
||||
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
BEGIN;
|
||||
@@ -1,5 +1,3 @@
|
||||
-- This migration is part of v2
|
||||
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
BEGIN;
|
||||
@@ -10,8 +8,8 @@ CREATE TABLE users_new
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME,
|
||||
username TEXT COLLATE NOCASE NOT NULL UNIQUE,
|
||||
email TEXT UNIQUE,
|
||||
first_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
first_name TEXT,
|
||||
last_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE oidc_apis;
|
||||
@@ -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);
|
||||
@@ -1,7 +0,0 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE one_time_access_tokens DROP COLUMN device_token;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -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;
|
||||
@@ -1,7 +0,0 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE signup_tokens_user_groups;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -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;
|
||||
@@ -1,7 +0,0 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE oidc_clients DROP COLUMN is_group_restricted;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,7 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
UPDATE app_config_variables SET value = 'ldapAttributeAdminGroup' WHERE value = 'ldapAdminGroupName';
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
@@ -1,14 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
UPDATE app_config_variables
|
||||
SET key = 'ldapAdminGroupName'
|
||||
WHERE key = 'ldapAttributeAdminGroup'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM app_config_variables
|
||||
WHERE key = 'ldapAdminGroupName'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
@@ -1 +0,0 @@
|
||||
-- No-op
|
||||
@@ -1,52 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE users_new
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
username TEXT COLLATE NOCASE NOT NULL UNIQUE,
|
||||
email TEXT UNIQUE,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
ldap_id TEXT UNIQUE,
|
||||
locale TEXT,
|
||||
disabled BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO users_new (
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
username,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
display_name,
|
||||
is_admin,
|
||||
ldap_id,
|
||||
locale,
|
||||
disabled
|
||||
) SELECT
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
username,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
display_name,
|
||||
is_admin,
|
||||
ldap_id,
|
||||
locale,
|
||||
disabled FROM users;
|
||||
|
||||
DROP TABLE users;
|
||||
ALTER TABLE users_new RENAME TO users;
|
||||
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id:v2
|
||||
image: ghcr.io/pocket-id/pocket-id:v1
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
ports:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 URL’er 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 URL’er 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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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ä"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "システム"
|
||||
}
|
||||
|
||||
@@ -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": "시스템"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user