mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 05:54:01 +03:00
Compare commits
34 Commits
feat/add_f
...
v2.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9834a08843 | ||
|
|
4f40352497 | ||
|
|
975d3c79c6 | ||
|
|
2f0338211d | ||
|
|
9c1a8b3c87 | ||
|
|
ce4b89da65 | ||
|
|
0d40bf29ab | ||
|
|
ff26c4273a | ||
|
|
444f7ff2b0 | ||
|
|
a0cb574313 | ||
|
|
978ac87def | ||
|
|
59fe481af9 | ||
|
|
4f09de2cfc | ||
|
|
8f48d10d55 | ||
|
|
5c4d7ff877 | ||
|
|
c5a4ffa523 | ||
|
|
6449b28b24 | ||
|
|
90fbfd7038 | ||
|
|
9ec4683d18 | ||
|
|
027e6f078d | ||
|
|
33cceeafa8 | ||
|
|
544f4e63d8 | ||
|
|
86152d996c | ||
|
|
fbdb93f1a7 | ||
|
|
f8f7222468 | ||
|
|
f79a86cded | ||
|
|
626adbf14c | ||
|
|
2b94535ade | ||
|
|
e825a58b39 | ||
|
|
b85a81f9b1 | ||
|
|
a06d9d21e4 | ||
|
|
cbecbd088f | ||
|
|
3c42a713ce | ||
|
|
e7e0176316 |
2
.github/workflows/backend-linter.yml
vendored
2
.github/workflows/backend-linter.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Run Golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9.0.0
|
||||
with:
|
||||
version: v2.9.0
|
||||
version: v2.11.4
|
||||
args: --build-tags=exclude_frontend
|
||||
working-directory: backend
|
||||
only-new-issues: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,3 +1,81 @@
|
||||
## v2.6.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- restore login screen background from not showing up ([975d3c7](https://github.com/pocket-id/pocket-id/commit/975d3c79c6a882291c69b31d25bfcd8b7896528c) by @kmendell)
|
||||
|
||||
### Other
|
||||
|
||||
- ignore webauthn type for swagger generation ([ce4b89d](https://github.com/pocket-id/pocket-id/commit/ce4b89da650f025747fd0dd45eab5cebe29f5a93) by @kmendell)
|
||||
- update golangci-lint ([#1440](https://github.com/pocket-id/pocket-id/pull/1440) by @ItalyPaleAle)
|
||||
- Add catalan language ([#1436](https://github.com/pocket-id/pocket-id/pull/1436) by @mcasellas)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.6.0...v2.6.1
|
||||
|
||||
## v2.6.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- disable callback URLs with protocols "javascript" and "data" ([#1397](https://github.com/pocket-id/pocket-id/pull/1397) by @ItalyPaleAle)
|
||||
- strip Root prefix from S3 List() returned paths ([#1413](https://github.com/pocket-id/pocket-id/pull/1413) by @vtmocanu)
|
||||
- use valid Tailwind v4 transition class for auth animation squares ([#1415](https://github.com/pocket-id/pocket-id/pull/1415) by @CoolShades)
|
||||
- resolve posixGroup memberUid as bare usernames ([#1422](https://github.com/pocket-id/pocket-id/pull/1422) by @gucong3000)
|
||||
- prevent flickering if no background image is set on login page ([027e6f0](https://github.com/pocket-id/pocket-id/commit/027e6f078da0eec712ae22a04b37c86110cb262b) by @stonith404)
|
||||
- improve form input layout if description next to it is multi col ([9ec4683](https://github.com/pocket-id/pocket-id/commit/9ec4683d18036ba1945bffd4bce14ec4c2dff7f9) by @stonith404)
|
||||
- access token renewal bypasses important checks ([978ac87](https://github.com/pocket-id/pocket-id/commit/978ac87deffec58beaccd15aead975e91b94c8a5) by @stonith404)
|
||||
|
||||
### Features
|
||||
|
||||
- add ability to revoke passkeys of users as admin ([#1386](https://github.com/pocket-id/pocket-id/pull/1386) by @jose-d)
|
||||
- add auth method claim (`amr`) to tokens ([#1433](https://github.com/pocket-id/pocket-id/pull/1433) by @stonith404)
|
||||
- add TLS support for HTTP/2 server ([#1429](https://github.com/pocket-id/pocket-id/pull/1429) by @IngmarStein)
|
||||
- add OpenID Connect `prompt` Parameter Handling ([#1299](https://github.com/pocket-id/pocket-id/pull/1299) by @rjaakke)
|
||||
- return not found. on `/setup` if already completed ([444f7ff](https://github.com/pocket-id/pocket-id/commit/444f7ff2b0269c12f1dba334a37d7db2007e172f) by @stonith404)
|
||||
|
||||
### Other
|
||||
|
||||
- update AAGUIDs ([#1403](https://github.com/pocket-id/pocket-id/pull/1403) by @github-actions[bot])
|
||||
- upgrade dependencies ([f8f7222](https://github.com/pocket-id/pocket-id/commit/f8f7222468dad90f630ae18f7c3fd78e37ba3f77) by @stonith404)
|
||||
- combobox not closed in e2e test ([fbdb93f](https://github.com/pocket-id/pocket-id/commit/fbdb93f1a768a05e6e3f2c6fd32b5de50a745bc6) by @stonith404)
|
||||
- Security upgrade alpine from latest to 3.23.4 ([#1431](https://github.com/pocket-id/pocket-id/pull/1431) by @stonith404)
|
||||
- security upgrade alpine from latest to 3.23.4 ([#1432](https://github.com/pocket-id/pocket-id/pull/1432) by @stonith404)
|
||||
- add Catalan language files ([4f09de2](https://github.com/pocket-id/pocket-id/commit/4f09de2cfc7d1e92632116821493a670fc7ee80d) by @stonith404)
|
||||
- reduce complexity of `ValidateEnvConfig` and `initRouter` ([a0cb574](https://github.com/pocket-id/pocket-id/commit/a0cb57431372c2bcc59904342597845e92a42a93) by @stonith404)
|
||||
- pass context to `shutdownServer` ([ff26c42](https://github.com/pocket-id/pocket-id/commit/ff26c4273a061b7d2c84e7b74f1e0f9e0acc6eb0) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.5.0...v2.6.0
|
||||
|
||||
## v2.5.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- better error messages when there's another instance of Pocket ID running ([#1370](https://github.com/pocket-id/pocket-id/pull/1370) by @ItalyPaleAle)
|
||||
- move tooltip inside of form input to prevent shifting ([#1369](https://github.com/pocket-id/pocket-id/pull/1369) by @GameTec-live)
|
||||
- derive LDAP admin access from group membership ([#1374](https://github.com/pocket-id/pocket-id/pull/1374) by @kmendell)
|
||||
- avoid fmt.Sprintf on custom GeoLiteDBUrl without %s placeholder ([#1384](https://github.com/pocket-id/pocket-id/pull/1384) by @choyri)
|
||||
- show a warning when SQLite DB is stored on NFS/SMB/FUSE ([#1381](https://github.com/pocket-id/pocket-id/pull/1381) by @ItalyPaleAle)
|
||||
- empty background restore after reboot ([#1379](https://github.com/pocket-id/pocket-id/pull/1379) by @taoso)
|
||||
- allow one-char username on signup ([#1378](https://github.com/pocket-id/pocket-id/pull/1378) by @taoso)
|
||||
|
||||
### Features
|
||||
|
||||
- allow use of svg, png, and ico images types for favicon ([#1289](https://github.com/pocket-id/pocket-id/pull/1289) by @taoso)
|
||||
- allow clearing background image ([#1290](https://github.com/pocket-id/pocket-id/pull/1290) by @taoso)
|
||||
- add `token_endpoint_auth_methods_supported` to `.well-known` ([#1388](https://github.com/pocket-id/pocket-id/pull/1388) by @owenvoke)
|
||||
- add TRUSTED_PLATFORM environment variable for gin ([#1372](https://github.com/pocket-id/pocket-id/pull/1372) by @choyri)
|
||||
|
||||
### Other
|
||||
|
||||
- add pr quality action ([e3905cf](https://github.com/pocket-id/pocket-id/commit/e3905cf3159fe0370778b0d7d3be64b4246d19be) by @stonith404)
|
||||
- separate querying LDAP and updating DB during sync ([#1371](https://github.com/pocket-id/pocket-id/pull/1371) by @ItalyPaleAle)
|
||||
- bump google.golang.org/grpc from 1.79.1 to 1.79.3 in /backend in the go_modules group across 1 directory ([#1391](https://github.com/pocket-id/pocket-id/pull/1391) by @dependabot[bot])
|
||||
- Improve Latvian translations in lv.json ([#1382](https://github.com/pocket-id/pocket-id/pull/1382) by @Raito00)
|
||||
- ignore linter on app image bootstrap ([5251cd9](https://github.com/pocket-id/pocket-id/commit/5251cd97994177c96cb6f9ab3f88ca31367b5b55) by @kmendell)
|
||||
- upgrade dependencies ([e7e0176](https://github.com/pocket-id/pocket-id/commit/e7e0176316857186b9683e2f0cb0686189f86cfb) by @kmendell)
|
||||
- upgrade dependencies ([3c42a71](https://github.com/pocket-id/pocket-id/commit/3c42a713ce91b4061ffcf86d92cbb19294359ff8) by @kmendell)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.4.0...v2.5.0
|
||||
|
||||
## v2.4.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
170
backend/go.mod
170
backend/go.mod
@@ -3,100 +3,101 @@ module github.com/pocket-id/pocket-id/backend
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
||||
github.com/aws/smithy-go v1.24.1
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
|
||||
github.com/aws/smithy-go v1.24.3
|
||||
github.com/caarlos0/env/v11 v11.4.0
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||
github.com/emersion/go-smtp v0.24.0
|
||||
github.com/gin-contrib/slog v1.2.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/gin-contrib/slog v1.2.1
|
||||
github.com/gin-gonic/gin v1.12.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.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/go-co-op/gocron/v2 v2.20.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/go-playground/validator/v10 v10.30.2
|
||||
github.com/go-webauthn/webauthn v0.16.4
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
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.4
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.5
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13
|
||||
github.com/lmittmann/tint v1.1.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-isatty v0.0.21
|
||||
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/stretchr/testify v1.11.1
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/log v0.16.0
|
||||
go.opentelemetry.io/otel/metric v1.40.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/time v0.14.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/log v0.19.0
|
||||
go.opentelemetry.io/otel/metric v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/image v0.39.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/text v0.36.0
|
||||
golang.org/x/time v0.15.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
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.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // 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.17 // 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.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.14.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // 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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // 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/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/go-github/v39 v39.2.0 // indirect
|
||||
@@ -106,7 +107,7 @@ require (
|
||||
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.9.1 // 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,60 +116,63 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||
github.com/lestrrat-go/dsig v1.2.1 // 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/v2 v2.0.0 // indirect
|
||||
github.com/lib/pq v1.11.2 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.42 // 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/nlnwa/whatwg-url v0.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/nlnwa/whatwg-url v0.6.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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.5 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.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/tinylib/msgp v1.6.3 // 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.10 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.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.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/arch v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.68.0 // indirect
|
||||
modernc.org/libc v1.71.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
modernc.org/sqlite v1.48.2 // indirect
|
||||
)
|
||||
|
||||
397
backend/go.sum
397
backend/go.sum
@@ -6,57 +6,57 @@ 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.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/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.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
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.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
|
||||
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.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
|
||||
github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg=
|
||||
github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
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/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA=
|
||||
github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
|
||||
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -72,8 +72,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
@@ -101,26 +101,28 @@ github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/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=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/gin-contrib/slog v1.2.1 h1:tQbsmllW/PNgtvHRdVlI38jLfpN4IFLS7Pb4HgTeiYw=
|
||||
github.com/gin-contrib/slog v1.2.1/go.mod h1:f/Ke0A3h4DUh0cQnjR2b/l+i0EmVJ+6VY6GIw3RKtxA=
|
||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.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.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
||||
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||
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-co-op/gocron/v2 v2.20.0 h1:9IMrnnVSWjfSh3E54gWmWCHbloQJLh6f9+nwyKfLNpc=
|
||||
github.com/go-co-op/gocron/v2 v2.20.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -132,16 +134,16 @@ 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.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.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.2.1 h1:/oB8i0FhSANuoN+YJF5XHMtppa7zGEYaQrrf6ytotjc=
|
||||
github.com/go-webauthn/x v0.2.1/go.mod h1:Wm0X0zXkzznit4gHj4m82GiBZRMEm+TDUIoJWIQLsE4=
|
||||
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/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
|
||||
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
|
||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -166,6 +168,8 @@ github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfh
|
||||
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-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
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=
|
||||
@@ -183,8 +187,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.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
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=
|
||||
@@ -225,26 +229,26 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||
github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc=
|
||||
github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||
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.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
|
||||
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.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/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.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
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=
|
||||
@@ -262,8 +266,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nlnwa/whatwg-url v0.5.0 h1:l71cqfqG44+VCQZQX3wD4bwheFWicPxuwaCimLEfpDo=
|
||||
github.com/nlnwa/whatwg-url v0.5.0/go.mod h1:X/ejnFFVbaOWdSul+cnlsSHviCzGZJdvPkgc9zD8IY8=
|
||||
github.com/nlnwa/whatwg-url v0.6.2 h1:jU61lU2ig4LANydbEJmA2nPrtCGiKdtgT0rmMd2VZ/Q=
|
||||
github.com/nlnwa/whatwg-url v0.6.2/go.mod h1:x0FPXJzzOEieQtsBT/AKvbiBbQ46YlL6Xa7m02M1ECk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
@@ -272,8 +276,10 @@ github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsT
|
||||
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/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/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -287,8 +293,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
@@ -319,6 +325,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
@@ -328,150 +336,171 @@ github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz3
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
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.15.0 h1:yOYhGNPZseueTTvWp5iBD3/CthrmvayUXYEX862dDi4=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0/go.mod h1:CvaNVqIfcybc+7xqZNubbE+26K6P7AKZF/l0lE2kdCk=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0 h1:LSJsvNqhj2sBNFb5NWHbyDK4QJ/skQ2ydjeOZ9OYNZ4=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0/go.mod h1:0Q5ocj6h/+C6KYq8cnl4tDFVd4I1HBdsJ440aeagHos=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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.opentelemetry.io/contrib/bridges/otelslog v0.18.0 h1:hhPGP3zvvy1xWT9RTy970wlniSxFttBIsAK1gvMguJM=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0/go.mod h1:twJF7inoMza6kxMcF8JOdL3mPmtOZu7GEr34CUNE6Dg=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0 h1:5FXSL2s6afUC1bzNzl1iedZZ8yqR7GOhbCoEXtyeK6Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0/go.mod h1:MdHW7tLtkeGJnR4TyOrnd5D0zUGZQB1l84uHCe8hRpE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/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/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
|
||||
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -484,20 +513,20 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
||||
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/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.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
modernc.org/libc v1.71.0 h1:bu0djXJGhqed3DnBzyzu3sY0fv432lesyz99ecEahyA=
|
||||
modernc.org/libc v1.71.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
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=
|
||||
@@ -506,8 +535,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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
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=
|
||||
|
||||
@@ -2,6 +2,7 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -10,8 +11,11 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
sloggin "github.com/gin-contrib/slog"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
@@ -32,6 +36,53 @@ import (
|
||||
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services)
|
||||
|
||||
func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
r, err := initEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registerRoutes(r, db, svc)
|
||||
|
||||
serverConfig, err := initServer(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runFn := func(ctx context.Context) error {
|
||||
return runServer(ctx, serverConfig)
|
||||
}
|
||||
|
||||
return runFn, nil
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
addr string
|
||||
certProvider *tlsCertProvider
|
||||
listener net.Listener
|
||||
server *http.Server
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func initEngine() (*gin.Engine, error) {
|
||||
setGinMode()
|
||||
|
||||
r := gin.New()
|
||||
initLogger(r)
|
||||
configureEngine(r)
|
||||
registerGlobalMiddleware(r)
|
||||
|
||||
frontendRateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(100*time.Millisecond), 300)
|
||||
if err := frontend.RegisterFrontend(r, frontendRateLimitMiddleware); err != nil {
|
||||
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
||||
return r, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to register frontend: %w", err)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func setGinMode() {
|
||||
// Set the appropriate Gin mode based on the environment
|
||||
switch common.EnvConfig.AppEnv {
|
||||
case common.AppEnvProduction:
|
||||
@@ -41,10 +92,9 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
case common.AppEnvTest:
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
initLogger(r)
|
||||
|
||||
func configureEngine(r *gin.Engine) {
|
||||
if !common.EnvConfig.TrustProxy {
|
||||
_ = r.SetTrustedProxies(nil)
|
||||
}
|
||||
@@ -56,34 +106,26 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
if common.EnvConfig.TracingEnabled {
|
||||
r.Use(otelgin.Middleware(common.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// Setup global middleware
|
||||
func registerGlobalMiddleware(r *gin.Engine) {
|
||||
r.Use(middleware.HeadMiddleware())
|
||||
r.Use(middleware.NewCacheControlMiddleware().Add())
|
||||
r.Use(middleware.NewCorsMiddleware().Add())
|
||||
r.Use(middleware.NewCspMiddleware().Add())
|
||||
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||
}
|
||||
|
||||
frontendRateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(100*time.Millisecond), 300)
|
||||
err := frontend.RegisterFrontend(r, frontendRateLimitMiddleware)
|
||||
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to register frontend: %w", err)
|
||||
}
|
||||
|
||||
// Initialize middleware for specific routes
|
||||
func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services) {
|
||||
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
|
||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||
|
||||
apiRateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 100)
|
||||
|
||||
// Set up API routes
|
||||
apiGroup := r.Group("/api", apiRateLimitMiddleware)
|
||||
controller.NewApiKeyController(apiGroup, authMiddleware, svc.apiKeyService)
|
||||
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.webauthnService, svc.appConfigService)
|
||||
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.oneTimeAccessService, svc.appConfigService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.oneTimeAccessService, svc.webauthnService, svc.appConfigService)
|
||||
controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService)
|
||||
controller.NewAppImagesController(apiGroup, authMiddleware, svc.appImagesService)
|
||||
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
|
||||
@@ -93,30 +135,81 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
controller.NewScimController(apiGroup, authMiddleware, svc.scimService)
|
||||
controller.NewUserSignupController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userSignUpService, svc.appConfigService)
|
||||
|
||||
// Add test controller in non-production environments
|
||||
if !common.EnvConfig.AppEnv.IsProduction() {
|
||||
for _, f := range registerTestControllers {
|
||||
f(apiGroup, db, svc)
|
||||
}
|
||||
}
|
||||
registerTestRoutes(apiGroup, db, svc)
|
||||
|
||||
// Set up base routes
|
||||
baseGroup := r.Group("/", apiRateLimitMiddleware)
|
||||
controller.NewWellKnownController(baseGroup, svc.jwtService)
|
||||
|
||||
// Set up healthcheck routes
|
||||
// These are not rate-limited
|
||||
// These are not rate-limited.
|
||||
controller.NewHealthzController(r)
|
||||
}
|
||||
|
||||
var protocols http.Protocols
|
||||
func registerTestRoutes(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
|
||||
if common.EnvConfig.AppEnv.IsProduction() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range registerTestControllers {
|
||||
f(apiGroup, db, svc)
|
||||
}
|
||||
}
|
||||
|
||||
func initServer(r *gin.Engine) (*serverConfig, error) {
|
||||
protocols, tlsConfig, certProvider, err := initServerProtocols()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
network, addr := listenerNetworkAndAddr()
|
||||
listener, err := net.Listen(network, addr) //nolint:noctx
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
||||
}
|
||||
|
||||
if err := setUnixSocketMode(network, addr); err != nil {
|
||||
listener.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &serverConfig{
|
||||
addr: addr,
|
||||
certProvider: certProvider,
|
||||
listener: listener,
|
||||
server: newHTTPServer(r, protocols),
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, error) {
|
||||
protocols := new(http.Protocols)
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
|
||||
// Set up the server
|
||||
srv := &http.Server{
|
||||
if common.EnvConfig.TLSCertFile == "" || common.EnvConfig.TLSKeyFile == "" {
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
return protocols, nil, nil, nil
|
||||
}
|
||||
|
||||
protocols.SetHTTP2(true)
|
||||
certProvider, err := newCertProvider(common.EnvConfig.TLSCertFile, common.EnvConfig.TLSKeyFile)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to load TLS certificate: %w", err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
GetCertificate: certProvider.GetCertificate,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
|
||||
slog.Info("TLS enabled")
|
||||
return protocols, tlsConfig, certProvider, nil
|
||||
}
|
||||
|
||||
func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
|
||||
return &http.Server{
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
Protocols: &protocols,
|
||||
Protocols: protocols,
|
||||
Handler: h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// HEAD requests don't get matched by Gin routes, so we convert them to GET
|
||||
// middleware.HeadMiddleware will convert them back to HEAD later
|
||||
@@ -129,73 +222,119 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
r.ServeHTTP(w, req)
|
||||
}), &http2.Server{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the listener
|
||||
network := "tcp"
|
||||
addr := net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
|
||||
if common.EnvConfig.UnixSocket != "" {
|
||||
network = "unix"
|
||||
addr = common.EnvConfig.UnixSocket
|
||||
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
|
||||
func listenerNetworkAndAddr() (string, string) {
|
||||
if common.EnvConfig.UnixSocket == "" {
|
||||
return "tcp", net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
|
||||
}
|
||||
|
||||
listener, err := net.Listen(network, addr) //nolint:noctx
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
||||
}
|
||||
|
||||
// Set the socket mode if using a Unix socket
|
||||
if network == "unix" && common.EnvConfig.UnixSocketMode != "" {
|
||||
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
|
||||
return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Service runner function
|
||||
runFn := func(ctx context.Context) error {
|
||||
slog.Info("Server listening", slog.String("addr", addr))
|
||||
|
||||
// Start the server in a background goroutine
|
||||
go func() {
|
||||
defer listener.Close()
|
||||
|
||||
// Next call blocks until the server is shut down
|
||||
srvErr := srv.Serve(listener)
|
||||
if srvErr != http.ErrServerClosed {
|
||||
slog.Error("Error starting app server", "error", srvErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Notify systemd that we are ready
|
||||
err = systemd.SdNotifyReady()
|
||||
if err != nil {
|
||||
// Log the error only
|
||||
slog.Warn("Unable to notify systemd that the service is ready", "error", err)
|
||||
}
|
||||
|
||||
// Block until the context is canceled
|
||||
<-ctx.Done()
|
||||
|
||||
// Handle graceful shutdown
|
||||
// Note we use the background context here as ctx has been canceled already
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck
|
||||
shutdownCancel()
|
||||
if shutdownErr != nil {
|
||||
// Log the error only (could be context canceled)
|
||||
slog.Warn("App server shutdown error", "error", shutdownErr)
|
||||
}
|
||||
addr := common.EnvConfig.UnixSocket
|
||||
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
|
||||
return "unix", addr
|
||||
}
|
||||
|
||||
func setUnixSocketMode(network, addr string) error {
|
||||
if network != "unix" || common.EnvConfig.UnixSocketMode == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return runFn, nil
|
||||
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
|
||||
return fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runServer(ctx context.Context, config *serverConfig) error {
|
||||
slog.Info("Server listening", slog.String("addr", config.addr), slog.Bool("tls", config.tlsConfig != nil))
|
||||
|
||||
certWatcher, err := startCertWatcher(ctx, config.certProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeCertWatcher(certWatcher)
|
||||
|
||||
startHTTPServer(config)
|
||||
notifySystemdReady()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
// We do not pass the context because it's already been canceled
|
||||
//nolint:contextcheck
|
||||
return shutdownServer(config.server)
|
||||
}
|
||||
|
||||
func startCertWatcher(ctx context.Context, certProvider *tlsCertProvider) (*fsnotify.Watcher, error) {
|
||||
if certProvider == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
certWatcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate watcher: %w", err)
|
||||
}
|
||||
|
||||
if err := certWatcher.Add(common.EnvConfig.TLSCertFile); err != nil {
|
||||
certWatcher.Close()
|
||||
return nil, fmt.Errorf("failed to watch TLS certificate: %w", err)
|
||||
}
|
||||
if err := certWatcher.Add(common.EnvConfig.TLSKeyFile); err != nil {
|
||||
certWatcher.Close()
|
||||
return nil, fmt.Errorf("failed to watch TLS key: %w", err)
|
||||
}
|
||||
|
||||
go certProvider.StartWatching(ctx, certWatcher)
|
||||
return certWatcher, nil
|
||||
}
|
||||
|
||||
func closeCertWatcher(certWatcher *fsnotify.Watcher) {
|
||||
if certWatcher != nil {
|
||||
certWatcher.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func startHTTPServer(config *serverConfig) {
|
||||
go func() {
|
||||
defer config.listener.Close()
|
||||
|
||||
listener := config.listener
|
||||
if config.tlsConfig != nil {
|
||||
listener = tls.NewListener(config.listener, config.tlsConfig)
|
||||
}
|
||||
srvErr := config.server.Serve(listener)
|
||||
|
||||
if srvErr != http.ErrServerClosed {
|
||||
slog.Error("Error starting app server", "error", srvErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func notifySystemdReady() {
|
||||
err := systemd.SdNotifyReady()
|
||||
if err != nil {
|
||||
// Log the error only
|
||||
slog.Warn("Unable to notify systemd that the service is ready", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func shutdownServer(srv *http.Server) error {
|
||||
// Note we use the background context here as ctx has been canceled already
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck
|
||||
shutdownCancel()
|
||||
if shutdownErr != nil {
|
||||
// Log the error only (could be context canceled)
|
||||
slog.Warn("App server shutdown error", "error", shutdownErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initLogger(r *gin.Engine) {
|
||||
@@ -224,3 +363,99 @@ func initLogger(r *gin.Engine) {
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
// tlsCertProvider holds certificates that can be dynamically reloaded
|
||||
type tlsCertProvider struct {
|
||||
certMutex sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
certFile string
|
||||
keyFile string
|
||||
forceReload atomic.Bool
|
||||
}
|
||||
|
||||
// GetCertificate implements tls.GetCertificate interface for dynamic certificate loading
|
||||
func (p *tlsCertProvider) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if p.forceReload.Load() {
|
||||
p.certMutex.Lock()
|
||||
p.forceReload.Store(false)
|
||||
p.certMutex.Unlock()
|
||||
}
|
||||
|
||||
p.certMutex.RLock()
|
||||
defer p.certMutex.RUnlock()
|
||||
return p.cert, nil
|
||||
}
|
||||
|
||||
// newCertProvider creates a new certificate provider with initial certificates loaded
|
||||
func newCertProvider(certFile, keyFile string) (*tlsCertProvider, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tlsCertProvider{
|
||||
cert: &cert,
|
||||
certFile: certFile,
|
||||
keyFile: keyFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// reloadCertificate reloads the certificate from disk
|
||||
func (p *tlsCertProvider) reloadCertificate() error {
|
||||
cert, err := tls.LoadX509KeyPair(p.certFile, p.keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reload TLS certificate: %w", err)
|
||||
}
|
||||
|
||||
p.certMutex.Lock()
|
||||
p.cert = &cert
|
||||
p.certMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartWatching begins monitoring the certificate files for changes with debouncing
|
||||
func (p *tlsCertProvider) StartWatching(ctx context.Context, watcher *fsnotify.Watcher) {
|
||||
debounceDuration := 1 * time.Second
|
||||
reloadTimer := time.NewTimer(debounceDuration)
|
||||
reloadTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Only process write/rename events for certificate/key files
|
||||
if event.Has(fsnotify.Write | fsnotify.Rename) {
|
||||
// Reset the debounce timer whenever we get a relevant event
|
||||
reloadTimer.Stop()
|
||||
// Drain the channel if there's a pending value
|
||||
select {
|
||||
case <-reloadTimer.C:
|
||||
default:
|
||||
}
|
||||
reloadTimer.Reset(debounceDuration)
|
||||
slog.Debug("TLS file change detected, debouncing", slog.String("path", event.Name))
|
||||
}
|
||||
case <-reloadTimer.C:
|
||||
// Timer fired - no more events in 500ms, so reload
|
||||
slog.Info("Reloading TLS certificate")
|
||||
|
||||
if err := p.reloadCertificate(); err != nil {
|
||||
slog.Error("Failed to reload TLS certificate", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
p.forceReload.Store(true)
|
||||
slog.Info("TLS certificate reloaded successfully")
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
slog.Error("Certificate watcher error", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ type EnvConfigSchema struct {
|
||||
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
|
||||
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
|
||||
|
||||
TLSCertFile string `env:"TLS_CERT" options:"file"`
|
||||
TLSKeyFile string `env:"TLS_KEY" options:"file"`
|
||||
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
|
||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
|
||||
@@ -142,6 +145,31 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
|
||||
return errors.New("ENCRYPTION_KEY must be at least 16 bytes long")
|
||||
}
|
||||
|
||||
prepareDbConfig(config)
|
||||
|
||||
if err := validateAppURLs(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFileBackend(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateLocalIPv6Ranges(config.LocalIPv6Ranges); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.AuditLogRetentionDays <= 0 {
|
||||
return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0")
|
||||
}
|
||||
|
||||
if config.StaticApiKey != "" && len(config.StaticApiKey) < 16 {
|
||||
return errors.New("STATIC_API_KEY must be at least 16 characters long")
|
||||
}
|
||||
|
||||
return validateTLSConfig(config)
|
||||
|
||||
}
|
||||
|
||||
func prepareDbConfig(config *EnvConfigSchema) {
|
||||
switch {
|
||||
case config.DbConnectionString == "":
|
||||
config.DbProvider = DbProviderSqlite
|
||||
@@ -151,64 +179,95 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
|
||||
default:
|
||||
config.DbProvider = DbProviderSqlite
|
||||
}
|
||||
}
|
||||
|
||||
parsedAppUrl, err := url.Parse(config.AppURL)
|
||||
if err != nil {
|
||||
return errors.New("APP_URL is not a valid URL")
|
||||
}
|
||||
if parsedAppUrl.Path != "" {
|
||||
return errors.New("APP_URL must not contain a path")
|
||||
func validateAppURLs(config *EnvConfigSchema) error {
|
||||
if err := validateURLWithoutPath(config.AppURL, "APP_URL"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Derive INTERNAL_APP_URL from APP_URL if not set; validate only when provided
|
||||
if config.InternalAppURL == "" {
|
||||
config.InternalAppURL = config.AppURL
|
||||
} else {
|
||||
parsedInternalAppUrl, err := url.Parse(config.InternalAppURL)
|
||||
if err != nil {
|
||||
return errors.New("INTERNAL_APP_URL is not a valid URL")
|
||||
}
|
||||
if parsedInternalAppUrl.Path != "" {
|
||||
return errors.New("INTERNAL_APP_URL must not contain a path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return validateURLWithoutPath(config.InternalAppURL, "INTERNAL_APP_URL")
|
||||
}
|
||||
|
||||
func validateURLWithoutPath(rawURL, envName string) error {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is not a valid URL", envName)
|
||||
}
|
||||
if parsedURL.Path != "" {
|
||||
return fmt.Errorf("%s must not contain a path", envName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFileBackend(config *EnvConfigSchema) error {
|
||||
switch config.FileBackend {
|
||||
case "s3", "database":
|
||||
// All good, these are valid values
|
||||
return nil
|
||||
case "", "filesystem":
|
||||
if config.UploadPath == "" {
|
||||
config.UploadPath = defaultFsUploadPath
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid FILE_BACKEND value. Must be 'filesystem', 'database', or 's3'")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate LOCAL_IPV6_RANGES
|
||||
ranges := strings.SplitSeq(config.LocalIPv6Ranges, ",")
|
||||
func validateLocalIPv6Ranges(localIPv6Ranges string) error {
|
||||
ranges := strings.SplitSeq(localIPv6Ranges, ",")
|
||||
for rangeStr := range ranges {
|
||||
rangeStr = strings.TrimSpace(rangeStr)
|
||||
if rangeStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid LOCAL_IPV6_RANGES '%s': %w", rangeStr, err)
|
||||
if err := validateLocalIPv6Range(rangeStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ipNet.IP.To4() != nil {
|
||||
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if config.AuditLogRetentionDays <= 0 {
|
||||
return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0")
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLocalIPv6Range(rangeStr string) error {
|
||||
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid LOCAL_IPV6_RANGES '%s': %w", rangeStr, err)
|
||||
}
|
||||
|
||||
if config.StaticApiKey != "" && len(config.StaticApiKey) < 16 {
|
||||
return errors.New("STATIC_API_KEY must be at least 16 characters long")
|
||||
if ipNet.IP.To4() != nil {
|
||||
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTLSConfig(config *EnvConfigSchema) error {
|
||||
switch {
|
||||
case config.TLSCertFile != "" && config.TLSKeyFile == "":
|
||||
return errors.New("TLS_KEY_FILE must be set when TLS_CERT_FILE is set")
|
||||
case config.TLSCertFile == "" && config.TLSKeyFile != "":
|
||||
return errors.New("TLS_CERT_FILE must be set when TLS_KEY_FILE is set")
|
||||
}
|
||||
|
||||
if config.TLSCertFile != "" && config.TLSKeyFile != "" {
|
||||
if _, err := os.Stat(config.TLSCertFile); err != nil {
|
||||
return fmt.Errorf("TLS_CERT_FILE not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.TLSCertFile != "" && config.TLSKeyFile != "" {
|
||||
if _, err := os.Stat(config.TLSKeyFile); err != nil {
|
||||
return fmt.Errorf("TLS_KEY_FILE not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -288,6 +347,7 @@ func resolveFileBasedEnvVariable(field reflect.Value, fieldType reflect.StructFi
|
||||
return nil
|
||||
}
|
||||
|
||||
// #nosec G703 - Path is passed by the admin
|
||||
fileContent, err := os.ReadFile(envVarFileValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file for env var %s: %w", envVarFileName, err)
|
||||
|
||||
@@ -207,6 +207,58 @@ func TestParseEnvConfig(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "invalid FILE_BACKEND value")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS cert is set without key", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
t.Setenv("TLS_CERT", "/path/to/cert.pem")
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_KEY_FILE must be set when TLS_CERT_FILE is set")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS key is set without cert", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
t.Setenv("TLS_KEY", "/path/to/key.pem")
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_CERT_FILE must be set when TLS_KEY_FILE is set")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS cert file does not exist", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
t.Setenv("TLS_CERT", "/nonexistent/cert.pem")
|
||||
|
||||
keyFile := t.TempDir() + "/key.pem"
|
||||
require.NoError(t, os.WriteFile(keyFile, []byte("key"), 0600))
|
||||
t.Setenv("TLS_KEY", keyFile)
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_CERT_FILE not found")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS key file does not exist", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
|
||||
certFile := t.TempDir() + "/cert.pem"
|
||||
require.NoError(t, os.WriteFile(certFile, []byte("cert"), 0600))
|
||||
t.Setenv("TLS_CERT", certFile)
|
||||
t.Setenv("TLS_KEY", "/nonexistent/key.pem")
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_KEY_FILE not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
||||
@@ -220,7 +272,7 @@ func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dbConnFile := tempDir + "/db_connection.txt"
|
||||
dbConnContent := "postgres://user:pass@localhost/testdb"
|
||||
dbConnContent := "postgres://user:pass@localhost/testdb" // #nosec G101 - test credential
|
||||
err = os.WriteFile(dbConnFile, []byte(dbConnContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -254,4 +306,26 @@ func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, binaryKeyContent, config.EncryptionKey)
|
||||
})
|
||||
|
||||
t.Run("should load TLS cert and key file contents", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
|
||||
certFile := tempDir + "/cert.pem"
|
||||
certContent := "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"
|
||||
err := os.WriteFile(certFile, []byte(certContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyFile := tempDir + "/key.pem"
|
||||
keyContent := "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----"
|
||||
err = os.WriteFile(keyFile, []byte(keyContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv("TLS_CERT_FILE", certFile)
|
||||
t.Setenv("TLS_KEY_FILE", keyFile)
|
||||
|
||||
err = prepareEnvConfig(&config)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, certContent, config.TLSCertFile)
|
||||
assert.Equal(t, keyContent, config.TLSKeyFile)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ func (e *AlreadyInUseError) Is(target error) bool {
|
||||
return errors.As(target, &x)
|
||||
}
|
||||
|
||||
type SetupAlreadyCompletedError struct{}
|
||||
type SetupNotAvailableError struct{}
|
||||
|
||||
func (e *SetupAlreadyCompletedError) Error() string { return "setup already completed" }
|
||||
func (e *SetupAlreadyCompletedError) HttpStatusCode() int { return http.StatusConflict }
|
||||
func (e *SetupNotAvailableError) Error() string { return "not found" }
|
||||
func (e *SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type TokenInvalidOrExpiredError struct{}
|
||||
|
||||
@@ -448,3 +448,27 @@ func (e *InvalidEmailVerificationTokenError) Error() string {
|
||||
func (e *InvalidEmailVerificationTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
// OIDC prompt parameter errors - used for redirect error responses
|
||||
|
||||
type OidcLoginRequiredError struct{}
|
||||
|
||||
func (e *OidcLoginRequiredError) Error() string { return "login_required" }
|
||||
func (e *OidcLoginRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcConsentRequiredError struct{}
|
||||
|
||||
func (e *OidcConsentRequiredError) Error() string { return "consent_required" }
|
||||
func (e *OidcConsentRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInteractionRequiredError struct{}
|
||||
|
||||
func (e *OidcInteractionRequiredError) Error() string { return "interaction_required" }
|
||||
func (e *OidcInteractionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcAccountSelectionRequiredError struct{}
|
||||
|
||||
func (e *OidcAccountSelectionRequiredError) Error() string {
|
||||
return "account_selection_required"
|
||||
}
|
||||
func (e *OidcAccountSelectionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
@@ -63,6 +63,7 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
// Add device information to the logs
|
||||
for i, logsDto := range logsDtos {
|
||||
logsDto.Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDto.ActorUsername = logsDto.Data["actorUsername"]
|
||||
logsDtos[i] = logsDto
|
||||
}
|
||||
|
||||
@@ -101,6 +102,7 @@ func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
||||
for i, logsDto := range logsDtos {
|
||||
logsDto.Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDto.Username = logs[i].User.Username
|
||||
logsDto.ActorUsername = logsDto.Data["actorUsername"]
|
||||
logsDtos[i] = logsDto
|
||||
}
|
||||
|
||||
|
||||
@@ -89,13 +89,29 @@ type OidcController struct {
|
||||
// @Router /api/oidc/authorize [post]
|
||||
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientRequestDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
code, callbackURL, err := oc.oidcService.Authorize(c.Request.Context(), input, c.GetString("userID"), c.ClientIP(), c.Request.UserAgent())
|
||||
code, callbackURL, err := oc.oidcService.Authorize(
|
||||
c.Request.Context(),
|
||||
input,
|
||||
c.GetString("userID"),
|
||||
c.GetString("authenticationMethod"),
|
||||
c.ClientIP(),
|
||||
c.Request.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
// Check if this is a prompt-related error that should be returned as a redirect error
|
||||
if isOidcPromptError(err) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"error": err.Error(),
|
||||
"requiresRedirect": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -109,6 +125,19 @@ func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// isOidcPromptError checks if an error is a prompt-related OIDC error that should trigger a redirect
|
||||
func isOidcPromptError(err error) bool {
|
||||
var loginReq *common.OidcLoginRequiredError
|
||||
var consentReq *common.OidcConsentRequiredError
|
||||
var interactionReq *common.OidcInteractionRequiredError
|
||||
var accountSelectionReq *common.OidcAccountSelectionRequiredError
|
||||
|
||||
return errors.As(err, &loginReq) ||
|
||||
errors.As(err, &consentReq) ||
|
||||
errors.As(err, &interactionReq) ||
|
||||
errors.As(err, &accountSelectionReq)
|
||||
}
|
||||
|
||||
// authorizationConfirmationRequiredHandler godoc
|
||||
// @Summary Check if authorization confirmation is required
|
||||
// @Description Check if the user needs to confirm authorization for the client
|
||||
@@ -801,7 +830,14 @@ func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) {
|
||||
ipAddress := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
err := oc.oidcService.VerifyDeviceCode(c.Request.Context(), userCode, c.GetString("userID"), ipAddress, userAgent)
|
||||
err := oc.oidcService.VerifyDeviceCode(
|
||||
c.Request.Context(),
|
||||
userCode,
|
||||
c.GetString("userID"),
|
||||
c.GetString("authenticationMethod"),
|
||||
ipAddress,
|
||||
userAgent)
|
||||
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -857,7 +893,13 @@ func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
preview, err := oc.oidcService.GetClientPreview(c.Request.Context(), clientID, userID, strings.Split(scopes, " "))
|
||||
preview, err := oc.oidcService.GetClientPreview(
|
||||
c.Request.Context(),
|
||||
clientID,
|
||||
userID,
|
||||
strings.Split(scopes, " "),
|
||||
c.GetString("authenticationMethod"))
|
||||
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -21,10 +21,11 @@ const defaultOneTimeAccessTokenDuration = 15 * time.Minute
|
||||
// @Summary User management controller
|
||||
// @Description Initializes all user-related API endpoints
|
||||
// @Tags Users
|
||||
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, appConfigService *service.AppConfigService) {
|
||||
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, webAuthnService *service.WebAuthnService, appConfigService *service.AppConfigService) {
|
||||
uc := UserController{
|
||||
userService: userService,
|
||||
oneTimeAccessService: oneTimeAccessService,
|
||||
webAuthnService: webAuthnService,
|
||||
appConfigService: appConfigService,
|
||||
}
|
||||
|
||||
@@ -34,8 +35,10 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
group.POST("/users", authMiddleware.Add(), uc.createUserHandler)
|
||||
group.PUT("/users/:id", authMiddleware.Add(), uc.updateUserHandler)
|
||||
group.GET("/users/:id/groups", authMiddleware.Add(), uc.getUserGroupsHandler)
|
||||
group.GET("/users/:id/webauthn-credentials", authMiddleware.Add(), uc.listUserWebauthnCredentialsHandler)
|
||||
group.PUT("/users/me", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserHandler)
|
||||
group.DELETE("/users/:id", authMiddleware.Add(), uc.deleteUserHandler)
|
||||
group.DELETE("/users/:id/webauthn-credentials/:credentialId", authMiddleware.Add(), uc.deleteUserWebauthnCredentialHandler)
|
||||
|
||||
group.PUT("/users/:id/user-groups", authMiddleware.Add(), uc.updateUserGroups)
|
||||
|
||||
@@ -60,6 +63,7 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
type UserController struct {
|
||||
userService *service.UserService
|
||||
oneTimeAccessService *service.OneTimeAccessService
|
||||
webAuthnService *service.WebAuthnService
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
@@ -87,6 +91,36 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, groupsDto)
|
||||
}
|
||||
|
||||
// listUserWebauthnCredentialsHandler godoc
|
||||
// @Summary List user passkeys
|
||||
// @Description Retrieve all WebAuthn credentials for a specific user
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {array} dto.WebauthnCredentialDto
|
||||
// @Router /api/users/{id}/webauthn-credentials [get]
|
||||
func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
if _, err := uc.userService.GetUser(c.Request.Context(), userID); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
credentials, err := uc.webAuthnService.ListCredentials(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var credentialDtos []dto.WebauthnCredentialDto
|
||||
if err := dto.MapStructList(credentials, &credentialDtos); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, credentialDtos)
|
||||
}
|
||||
|
||||
// listUsersHandler godoc
|
||||
// @Summary List users
|
||||
// @Description Get a paginated list of users with optional search and sorting
|
||||
@@ -181,6 +215,31 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// deleteUserWebauthnCredentialHandler godoc
|
||||
// @Summary Delete user passkey
|
||||
// @Description Delete a specific WebAuthn credential for a user
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Param credentialId path string true "Credential ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/users/{id}/webauthn-credentials/{credentialId} [delete]
|
||||
func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) {
|
||||
err := uc.webAuthnService.DeleteCredential(
|
||||
c.Request.Context(),
|
||||
c.Param("id"),
|
||||
c.Param("credentialId"),
|
||||
c.ClientIP(),
|
||||
c.Request.UserAgent(),
|
||||
c.GetString("userID"),
|
||||
)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// createUserHandler godoc
|
||||
// @Summary Create user
|
||||
// @Description Create a new user
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"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"
|
||||
@@ -30,6 +31,7 @@ func NewUserSignupController(group *gin.RouterGroup, authMiddleware *middleware.
|
||||
group.GET("/signup-tokens", authMiddleware.Add(), usc.listSignupTokensHandler)
|
||||
group.DELETE("/signup-tokens/:id", authMiddleware.Add(), usc.deleteSignupTokenHandler)
|
||||
group.POST("/signup", rateLimitMiddleware.Add(rate.Every(1*time.Minute), 10), usc.signupHandler)
|
||||
group.GET("/signup/setup", usc.checkInitialAdminSetupAvailable)
|
||||
group.POST("/signup/setup", usc.signUpInitialAdmin)
|
||||
|
||||
}
|
||||
@@ -39,6 +41,21 @@ type UserSignupController struct {
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
func (usc *UserSignupController) checkInitialAdminSetupAvailable(c *gin.Context) {
|
||||
setupCompleted, err := usc.userSignUpService.IsInitialAdminSetupCompleted(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if setupCompleted {
|
||||
_ = c.Error(&common.SetupNotAvailableError{})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// signUpInitialAdmin godoc
|
||||
// @Summary Sign up initial admin user
|
||||
// @Description Sign up and generate setup access token for initial admin user
|
||||
|
||||
@@ -137,7 +137,7 @@ func (wc *WebauthnController) deleteCredentialHandler(c *gin.Context) {
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
err := wc.webAuthnService.DeleteCredential(c.Request.Context(), userID, credentialID, clientIP, userAgent)
|
||||
err := wc.webAuthnService.DeleteCredential(c.Request.Context(), userID, credentialID, clientIP, userAgent, userID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -91,6 +91,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
||||
"id_token_signing_alg_values_supported": []string{alg.String()},
|
||||
"authorization_response_iss_parameter_supported": true,
|
||||
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||
"prompt_values_supported": []string{"none", "login", "consent"},
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "none"},
|
||||
}
|
||||
return json.Marshal(config)
|
||||
|
||||
@@ -8,12 +8,13 @@ type AuditLogDto struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||
|
||||
Event string `json:"event"`
|
||||
IpAddress string `json:"ipAddress"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Device string `json:"device"`
|
||||
UserID string `json:"userID"`
|
||||
Username string `json:"username"`
|
||||
Data map[string]string `json:"data"`
|
||||
Event string `json:"event"`
|
||||
IpAddress string `json:"ipAddress"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Device string `json:"device"`
|
||||
UserID string `json:"userID"`
|
||||
Username string `json:"username"`
|
||||
ActorUsername string `json:"actorUsername"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ type OidcClientWithAllowedGroupsCountDto struct {
|
||||
|
||||
type OidcClientUpdateDto struct {
|
||||
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
|
||||
CallbackURLs []string `json:"callbackURLs" binding:"omitempty,dive,callback_url"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs" binding:"omitempty,dive,callback_url"`
|
||||
CallbackURLs []string `json:"callbackURLs" binding:"omitempty,dive,callback_url_pattern"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs" binding:"omitempty,dive,callback_url_pattern"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
@@ -66,11 +66,12 @@ type OidcClientFederatedIdentityDto struct {
|
||||
type AuthorizeOidcClientRequestDto struct {
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
CallbackURL string `json:"callbackURL" binding:"omitempty,callback_url"`
|
||||
Nonce string `json:"nonce"`
|
||||
CodeChallenge string `json:"codeChallenge"`
|
||||
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
||||
ReauthenticationToken string `json:"reauthenticationToken"`
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientResponseDto struct {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
@@ -19,38 +21,38 @@ var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9_.@-]*[a-
|
||||
var validateClientIDRegex = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
|
||||
|
||||
func init() {
|
||||
v := binding.Validator.Engine().(*validator.Validate)
|
||||
engine := binding.Validator.Engine().(*validator.Validate)
|
||||
|
||||
// Maximum allowed value for TTLs
|
||||
const maxTTL = 31 * 24 * time.Hour
|
||||
|
||||
if err := v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
|
||||
return ValidateUsername(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for username: " + err.Error())
|
||||
validators := map[string]validator.Func{
|
||||
"username": func(fl validator.FieldLevel) bool {
|
||||
return ValidateUsername(fl.Field().String())
|
||||
},
|
||||
"client_id": func(fl validator.FieldLevel) bool {
|
||||
return ValidateClientID(fl.Field().String())
|
||||
},
|
||||
"ttl": func(fl validator.FieldLevel) bool {
|
||||
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Allow zero, which means the field wasn't set
|
||||
return ttl.Duration == 0 || (ttl.Duration > time.Second && ttl.Duration <= maxTTL)
|
||||
},
|
||||
"callback_url": func(fl validator.FieldLevel) bool {
|
||||
return ValidateCallbackURL(fl.Field().String())
|
||||
},
|
||||
"callback_url_pattern": func(fl validator.FieldLevel) bool {
|
||||
return ValidateCallbackURLPattern(fl.Field().String())
|
||||
},
|
||||
}
|
||||
|
||||
if err := v.RegisterValidation("client_id", func(fl validator.FieldLevel) bool {
|
||||
return ValidateClientID(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for client_id: " + err.Error())
|
||||
}
|
||||
|
||||
if err := v.RegisterValidation("ttl", func(fl validator.FieldLevel) bool {
|
||||
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
|
||||
if !ok {
|
||||
return false
|
||||
for k, v := range validators {
|
||||
err := engine.RegisterValidation(k, v)
|
||||
if err != nil {
|
||||
panic("Failed to register custom validation for " + k + ": " + err.Error())
|
||||
}
|
||||
// Allow zero, which means the field wasn't set
|
||||
return ttl.Duration == 0 || (ttl.Duration > time.Second && ttl.Duration <= maxTTL)
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for ttl: " + err.Error())
|
||||
}
|
||||
|
||||
if err := v.RegisterValidation("callback_url", func(fl validator.FieldLevel) bool {
|
||||
return ValidateCallbackURL(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for callback_url: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +66,24 @@ func ValidateClientID(clientID string) bool {
|
||||
return validateClientIDRegex.MatchString(clientID)
|
||||
}
|
||||
|
||||
// ValidateCallbackURL validates callback URLs with support for wildcards
|
||||
func ValidateCallbackURL(raw string) bool {
|
||||
// ValidateCallbackURL validates the input callback URL
|
||||
func ValidateCallbackURL(str string) bool {
|
||||
// Ensure the URL is a valid one and that the protocol is not "javascript:" or "data:"
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "javascript", "data":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCallbackURLPattern validates callback URL patterns, with support for wildcards
|
||||
func ValidateCallbackURLPattern(raw string) bool {
|
||||
err := utils.ValidateCallbackURLPattern(raw)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -57,3 +57,28 @@ func TestValidateClientID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCallbackURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"valid https URL", "https://example.com/callback", true},
|
||||
{"valid loopback URL", "http://127.0.0.1:49813/callback", true},
|
||||
{"empty scheme", "//127.0.0.1:49813/callback", true},
|
||||
{"valid custom scheme", "pocketid://callback", true},
|
||||
{"invalid malformed URL", "http://[::1", false},
|
||||
{"invalid missing scheme separator", "://example.com/callback", false},
|
||||
{"rejects javascript scheme", "javascript:alert(1)", false},
|
||||
{"rejects mixed case javascript scheme", "JavaScript:alert(1)", false},
|
||||
{"rejects data scheme", "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
||||
{"rejects mixed case data scheme", "DaTa:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, ValidateCallbackURL(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type WebauthnCredentialDto struct {
|
||||
Name string `json:"name"`
|
||||
CredentialID string `json:"credentialID"`
|
||||
AttestationType string `json:"attestationType"`
|
||||
Transport []protocol.AuthenticatorTransport `json:"transport"`
|
||||
Transport []protocol.AuthenticatorTransport `json:"transport" swaggertype:"array,string"`
|
||||
|
||||
BackupEligible bool `json:"backupEligible"`
|
||||
BackupState bool `json:"backupState"`
|
||||
|
||||
@@ -74,10 +74,11 @@ func (m *AuthMiddleware) WithApiKeyAuthDisabled() *AuthMiddleware {
|
||||
|
||||
func (m *AuthMiddleware) Add() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
|
||||
userID, isAdmin, authenticationMethod, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
|
||||
if err == nil {
|
||||
c.Set("userID", userID)
|
||||
c.Set("userIsAdmin", isAdmin)
|
||||
c.Set("authenticationMethod", authenticationMethod)
|
||||
if c.IsAborted() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
authMiddleware := NewAuthMiddleware(apiKeyService, userService, jwtService)
|
||||
|
||||
user := createUserForAuthMiddlewareTest(t, db)
|
||||
jwtToken, err := jwtService.GenerateAccessToken(user)
|
||||
jwtToken, err := jwtService.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, apiKeyToken, err := apiKeyService.CreateApiKey(t.Context(), user.ID, dto.ApiKeyCreateDto{
|
||||
@@ -60,7 +60,7 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("rejects API key auth when API key auth is disabled", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/protected", nil)
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/protected", nil)
|
||||
req.Header.Set("X-API-Key", apiKeyToken)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("allows JWT auth when API key auth is disabled", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/protected", nil)
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+jwtToken)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestCacheControlMiddlewareSetsDefault(t *testing.T) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
@@ -36,7 +36,7 @@ func TestCacheControlMiddlewarePreservesExistingHeader(t *testing.T) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/custom", http.NoBody)
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/custom", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@@ -20,7 +20,7 @@ func NewJwtAuthMiddleware(jwtService *service.JwtService, userService *service.U
|
||||
|
||||
func (m *JwtAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, isAdmin, err := m.Verify(c, adminRequired)
|
||||
userID, isAdmin, authenticationMethod, err := m.Verify(c, adminRequired)
|
||||
if err != nil {
|
||||
c.Abort()
|
||||
_ = c.Error(err)
|
||||
@@ -29,11 +29,12 @@ func (m *JwtAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc {
|
||||
|
||||
c.Set("userID", userID)
|
||||
c.Set("userIsAdmin", isAdmin)
|
||||
c.Set("authenticationMethod", authenticationMethod)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject string, isAdmin bool, err error) {
|
||||
func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject string, isAdmin bool, authenticationMethod string, err error) {
|
||||
// Extract the token from the cookie
|
||||
accessToken, err := c.Cookie(cookie.AccessTokenCookieName)
|
||||
if err != nil {
|
||||
@@ -41,33 +42,37 @@ func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject
|
||||
var ok bool
|
||||
_, accessToken, ok = strings.Cut(c.GetHeader("Authorization"), " ")
|
||||
if !ok || accessToken == "" {
|
||||
return "", false, &common.NotSignedInError{}
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := m.jwtService.VerifyAccessToken(accessToken)
|
||||
if err != nil {
|
||||
return "", false, &common.NotSignedInError{}
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
}
|
||||
authenticationMethod, err = service.GetAuthenticationMethod(token)
|
||||
if err != nil {
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
}
|
||||
|
||||
subject, ok := token.Subject()
|
||||
if !ok {
|
||||
_ = c.Error(&common.TokenInvalidError{})
|
||||
return
|
||||
return "", false, "", &common.TokenInvalidError{}
|
||||
}
|
||||
|
||||
user, err := m.userService.GetUser(c, subject)
|
||||
if err != nil {
|
||||
return "", false, &common.NotSignedInError{}
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
return "", false, &common.UserDisabledError{}
|
||||
return "", false, "", &common.UserDisabledError{}
|
||||
}
|
||||
|
||||
if adminRequired && !user.IsAdmin {
|
||||
return "", false, &common.MissingPermissionError{}
|
||||
return "", false, "", &common.MissingPermissionError{}
|
||||
}
|
||||
|
||||
return subject, isAdmin, nil
|
||||
return subject, user.IsAdmin, authenticationMethod, nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type OidcAuthorizationCode struct {
|
||||
|
||||
Code string
|
||||
Scope string
|
||||
AuthenticationMethod string
|
||||
Nonce string
|
||||
CodeChallenge *string
|
||||
CodeChallengeMethodSha256 *bool
|
||||
@@ -77,9 +78,10 @@ func (c OidcClient) HasDarkLogo() bool {
|
||||
type OidcRefreshToken struct {
|
||||
Base
|
||||
|
||||
Token string
|
||||
ExpiresAt datatype.DateTime
|
||||
Scope string
|
||||
Token string
|
||||
ExpiresAt datatype.DateTime
|
||||
Scope string
|
||||
AuthenticationMethod string
|
||||
|
||||
UserID string
|
||||
User User
|
||||
@@ -141,12 +143,13 @@ func (cu UrlList) Value() (driver.Value, error) {
|
||||
|
||||
type OidcDeviceCode struct {
|
||||
Base
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scope string
|
||||
Nonce string
|
||||
ExpiresAt datatype.DateTime
|
||||
IsAuthorized bool
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scope string
|
||||
AuthenticationMethod string
|
||||
Nonce string
|
||||
ExpiresAt datatype.DateTime
|
||||
IsAuthorized bool
|
||||
|
||||
UserID *string
|
||||
User User
|
||||
|
||||
@@ -265,9 +265,9 @@ func (s *AppConfigService) UpdateAppConfigValues(ctx context.Context, keysAndVal
|
||||
// We update the in-memory data (in the cfg struct) and collect values to update in the database
|
||||
// (Note the += 2, as we are iterating through key-value pairs)
|
||||
dbUpdate := make([]model.AppConfigVariable, 0, len(keysAndValues)/2)
|
||||
for i := 0; i < len(keysAndValues); i += 2 {
|
||||
key := keysAndValues[i]
|
||||
value := keysAndValues[i+1]
|
||||
for i := 1; i < len(keysAndValues); i += 2 {
|
||||
key := keysAndValues[i-1]
|
||||
value := keysAndValues[i]
|
||||
|
||||
// Ensure that the field is valid
|
||||
// We do this by grabbing the default value
|
||||
@@ -408,6 +408,7 @@ func (s *AppConfigService) loadDbConfigFromEnv(ctx context.Context, tx *gorm.DB)
|
||||
if attrs == "sensitive" {
|
||||
fileName := os.Getenv(envVarName + "_FILE")
|
||||
if fileName != "" {
|
||||
// #nosec G703 - Value is provided by admin
|
||||
b, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret '%s' from file '%s': %w", envVarName, fileName, err)
|
||||
|
||||
@@ -104,7 +104,7 @@ func newFileHeader(t *testing.T, filename string, content []byte) *multipart.Fil
|
||||
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
_, fileHeader, err := req.FormFile("file")
|
||||
|
||||
@@ -92,7 +92,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
|
||||
// If the user hasn't logged in from the same device before and email notifications are enabled, send an email
|
||||
if s.appConfigService.GetDbConfig().EmailLoginNotificationEnabled.IsTrue() && count <= 1 {
|
||||
// We use a background context here as this is running in a goroutine
|
||||
// #nosec G118 - We use a background context here as this is running in a goroutine
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
|
||||
@@ -245,20 +245,22 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
|
||||
authCodes := []model.OidcAuthorizationCode{
|
||||
{
|
||||
Code: "auth-code",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
Code: "auth-code",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
AuthenticationMethod: AuthenticationMethodPhishingResistant,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
},
|
||||
{
|
||||
Code: "federated",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[1].ID,
|
||||
ClientID: oidcClients[3].ID,
|
||||
Code: "federated",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
AuthenticationMethod: AuthenticationMethodPhishingResistant,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[1].ID,
|
||||
ClientID: oidcClients[3].ID,
|
||||
},
|
||||
}
|
||||
for _, authCode := range authCodes {
|
||||
@@ -268,11 +270,12 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
}
|
||||
|
||||
refreshToken := model.OidcRefreshToken{
|
||||
Token: utils.CreateSha256Hash("ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo"),
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
Token: utils.CreateSha256Hash("ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo"),
|
||||
AuthenticationMethod: AuthenticationMethodPhishingResistant,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
}
|
||||
if err := tx.Create(&refreshToken).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -32,6 +32,15 @@ const (
|
||||
// RefreshTokenClaim is the claim used for the refresh token's value
|
||||
RefreshTokenClaim = "rt"
|
||||
|
||||
// AuthenticationMethodsClaim is the claim used to identify how the user authenticated
|
||||
AuthenticationMethodsClaim = "amr"
|
||||
|
||||
// AuthenticationMethodPhishingResistant identifies phishing-resistant authentication, such as passkeys
|
||||
AuthenticationMethodPhishingResistant = "phr"
|
||||
|
||||
// AuthenticationMethodOneTimePassword identifies one-time password/code authentication
|
||||
AuthenticationMethodOneTimePassword = "otp"
|
||||
|
||||
// OAuthAccessTokenJWTType identifies a JWT as an OAuth access token
|
||||
OAuthAccessTokenJWTType = "oauth-access-token" //nolint:gosec
|
||||
|
||||
@@ -187,7 +196,8 @@ func (s *JwtService) SetKey(privateKey jwk.Key) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
||||
func (s *JwtService) GenerateAccessToken(user model.User, authenticationMethod string) (string, error) {
|
||||
|
||||
now := time.Now()
|
||||
token, err := jwt.NewBuilder().
|
||||
Subject(user.ID).
|
||||
@@ -215,6 +225,11 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
||||
return "", fmt.Errorf("failed to set 'isAdmin' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAuthenticationMethods(token, authenticationMethod)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to set '%s' claim in token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
alg, _ := s.privateKey.Algorithm()
|
||||
signed, err := jwt.Sign(token, jwt.WithKey(alg, s.privateKey))
|
||||
if err != nil {
|
||||
@@ -243,7 +258,7 @@ func (s *JwtService) VerifyAccessToken(tokenString string) (jwt.Token, error) {
|
||||
}
|
||||
|
||||
// BuildIDToken creates an ID token with all claims
|
||||
func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, nonce string) (jwt.Token, error) {
|
||||
func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, nonce string, authenticationMethod string) (jwt.Token, error) {
|
||||
now := time.Now()
|
||||
token, err := jwt.NewBuilder().
|
||||
Expiration(now.Add(1 * time.Hour)).
|
||||
@@ -265,6 +280,11 @@ func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, no
|
||||
return nil, fmt.Errorf("failed to set 'type' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAuthenticationMethods(token, authenticationMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set '%s' claim in token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
for k, v := range userClaims {
|
||||
err = token.Set(k, v)
|
||||
if err != nil {
|
||||
@@ -283,8 +303,8 @@ func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, no
|
||||
}
|
||||
|
||||
// GenerateIDToken creates and signs an ID token
|
||||
func (s *JwtService) GenerateIDToken(userClaims map[string]any, clientID string, nonce string) (string, error) {
|
||||
token, err := s.BuildIDToken(userClaims, clientID, nonce)
|
||||
func (s *JwtService) GenerateIDToken(userClaims map[string]any, clientID string, nonce string, authenticationMethod string) (string, error) {
|
||||
token, err := s.BuildIDToken(userClaims, clientID, nonce, authenticationMethod)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -332,7 +352,7 @@ func (s *JwtService) VerifyIdToken(tokenString string, acceptExpiredTokens bool)
|
||||
}
|
||||
|
||||
// BuildOAuthAccessToken creates an OAuth access token with all claims
|
||||
func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string) (jwt.Token, error) {
|
||||
func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string, authenticationMethod string) (jwt.Token, error) {
|
||||
now := time.Now()
|
||||
token, err := jwt.NewBuilder().
|
||||
Subject(user.ID).
|
||||
@@ -355,12 +375,17 @@ func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string) (jw
|
||||
return nil, fmt.Errorf("failed to set 'type' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAuthenticationMethods(token, authenticationMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set '%s' claim in token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GenerateOAuthAccessToken creates and signs an OAuth access token
|
||||
func (s *JwtService) GenerateOAuthAccessToken(user model.User, clientID string) (string, error) {
|
||||
token, err := s.BuildOAuthAccessToken(user, clientID)
|
||||
func (s *JwtService) GenerateOAuthAccessToken(user model.User, clientID string, authenticationMethod string) (string, error) {
|
||||
token, err := s.BuildOAuthAccessToken(user, clientID, authenticationMethod)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -534,6 +559,27 @@ func GetIsAdmin(token jwt.Token) (bool, error) {
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
// GetAuthenticationMethod returns the first authentication method in the "amr" claim in the token
|
||||
func GetAuthenticationMethod(token jwt.Token) (string, error) {
|
||||
if !token.Has(AuthenticationMethodsClaim) {
|
||||
return "", nil
|
||||
}
|
||||
var rawAuthenticationMethods []any
|
||||
err := token.Get(AuthenticationMethodsClaim, &rawAuthenticationMethods)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get '%s' claim from token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
if len(rawAuthenticationMethods) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
authenticationMethod, ok := rawAuthenticationMethods[0].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid '%s' claim in token: expected array of strings", AuthenticationMethodsClaim)
|
||||
}
|
||||
return authenticationMethod, nil
|
||||
}
|
||||
|
||||
// SetTokenType sets the "type" claim in the token
|
||||
func SetTokenType(token jwt.Token, tokenType string) error {
|
||||
if tokenType == "" {
|
||||
@@ -551,6 +597,14 @@ func SetIsAdmin(token jwt.Token, isAdmin bool) error {
|
||||
return token.Set(IsAdminClaim, isAdmin)
|
||||
}
|
||||
|
||||
// SetAuthenticationMethods sets the authentication method references claim in the token
|
||||
func SetAuthenticationMethods(token jwt.Token, authenticationMethod string) error {
|
||||
if authenticationMethod == "" {
|
||||
return nil
|
||||
}
|
||||
return token.Set(AuthenticationMethodsClaim, []string{authenticationMethod})
|
||||
}
|
||||
|
||||
// SetAudienceString sets the "aud" claim with a value that is a string, and not an array
|
||||
// This is permitted by RFC 7519, and it's done here for backwards-compatibility
|
||||
func SetAudienceString(token jwt.Token, audience string) error {
|
||||
|
||||
@@ -174,6 +174,7 @@ func TestJwtService_Init(t *testing.T) {
|
||||
_ = assert.True(t, ok) &&
|
||||
assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestJwtService_GetPublicJWK(t *testing.T) {
|
||||
@@ -308,7 +309,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -321,6 +322,9 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
isAdmin, err := GetIsAdmin(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get isAdmin claim") &&
|
||||
assert.False(t, isAdmin, "isAdmin should be false")
|
||||
authenticationMethod, err := GetAuthenticationMethod(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get amr claim") &&
|
||||
assert.Empty(t, authenticationMethod, "amr should be empty when not specified")
|
||||
audience, ok := claims.Audience()
|
||||
_ = assert.True(t, ok, "Audience not found in token") &&
|
||||
assert.Equal(t, []string{service.envConfig.AppURL}, audience, "Audience should contain the app URL")
|
||||
@@ -344,7 +348,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(adminUser)
|
||||
tokenString, err := service.GenerateAccessToken(adminUser, "")
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
|
||||
claims, err := service.VerifyAccessToken(tokenString)
|
||||
@@ -358,6 +362,24 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
assert.Equal(t, adminUser.ID, subject, "Token subject should match user ID")
|
||||
})
|
||||
|
||||
t.Run("sets authentication method references claim when provided", func(t *testing.T) {
|
||||
service, _, _ := setupJwtService(t, mockConfig)
|
||||
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "user-with-auth-method"},
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, AuthenticationMethodPhishingResistant)
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
|
||||
claims, err := service.VerifyAccessToken(tokenString)
|
||||
require.NoError(t, err, "Failed to verify generated token")
|
||||
|
||||
authenticationMethod, err := GetAuthenticationMethod(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get amr claim") &&
|
||||
assert.Equal(t, AuthenticationMethodPhishingResistant, authenticationMethod, "amr should match")
|
||||
})
|
||||
|
||||
t.Run("uses session duration from config", func(t *testing.T) {
|
||||
customMockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "30"}, // 30 minutes
|
||||
@@ -368,7 +390,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
Base: model.Base{ID: "user456"},
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
|
||||
claims, err := service.VerifyAccessToken(tokenString)
|
||||
@@ -396,7 +418,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err, "Failed to generate access token with Ed25519 key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -433,7 +455,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err, "Failed to generate access token with ECDSA key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -470,7 +492,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err, "Failed to generate access token with RSA key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -508,7 +530,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "test-client-123"
|
||||
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
require.NoError(t, err, "Failed to generate ID token")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -593,7 +615,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
const clientID = "test-client-456"
|
||||
nonce := "random-nonce-value"
|
||||
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, nonce)
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, nonce, "")
|
||||
require.NoError(t, err, "Failed to generate ID token with nonce")
|
||||
|
||||
publicKey, err := service.GetPublicJWK()
|
||||
@@ -614,7 +636,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
userClaims := map[string]any{
|
||||
"sub": "user789",
|
||||
}
|
||||
tokenString, err := service.GenerateIDToken(userClaims, "client-789", "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, "client-789", "", "")
|
||||
require.NoError(t, err, "Failed to generate ID token")
|
||||
|
||||
service.envConfig.AppURL = "https://wrong-issuer.com"
|
||||
@@ -640,7 +662,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "eddsa-client-123"
|
||||
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
require.NoError(t, err, "Failed to generate ID token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -677,7 +699,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "ecdsa-client-123"
|
||||
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
require.NoError(t, err, "Failed to generate ID token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -715,7 +737,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "rsa-client-123"
|
||||
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
require.NoError(t, err, "Failed to generate ID token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -745,7 +767,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "test-client-123"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate OAuth access token")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -772,6 +794,25 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
assert.InDelta(t, 0, timeDiff, 1.0, "Token should expire in approximately 1 hour")
|
||||
})
|
||||
|
||||
t.Run("sets authentication method references claim when provided", func(t *testing.T) {
|
||||
service, _, _ := setupJwtService(t, mockConfig)
|
||||
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "oauth-amr-user"},
|
||||
}
|
||||
const clientID = "test-client-amr"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, AuthenticationMethodPhishingResistant)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token")
|
||||
|
||||
claims, err := service.VerifyOAuthAccessToken(tokenString)
|
||||
require.NoError(t, err, "Failed to verify generated OAuth access token")
|
||||
|
||||
authenticationMethod, err := GetAuthenticationMethod(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get amr claim") &&
|
||||
assert.Equal(t, AuthenticationMethodPhishingResistant, authenticationMethod, "amr should match")
|
||||
})
|
||||
|
||||
t.Run("fails verification for expired token", func(t *testing.T) {
|
||||
service, _, _ := setupJwtService(t, mockConfig)
|
||||
|
||||
@@ -805,7 +846,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
user := model.User{Base: model.Base{ID: "user789"}}
|
||||
const clientID = "test-client-789"
|
||||
|
||||
tokenString, err := service1.GenerateOAuthAccessToken(user, clientID)
|
||||
tokenString, err := service1.GenerateOAuthAccessToken(user, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate OAuth access token")
|
||||
|
||||
_, err = service2.VerifyOAuthAccessToken(tokenString)
|
||||
@@ -828,7 +869,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "eddsa-oauth-client"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate OAuth access token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -865,7 +906,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "ecdsa-oauth-client"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate OAuth access token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -902,7 +943,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "rsa-oauth-client"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate OAuth access token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
|
||||
@@ -391,6 +391,12 @@ func (s *LdapService) resolveGroupMemberUsername(ctx context.Context, client lda
|
||||
return norm.NFC.String(username)
|
||||
}
|
||||
|
||||
// posixGroup (and similar) stores bare usernames in memberUid, not DNs. Treat any value
|
||||
// that is not a valid DN as the username directly — see https://github.com/pocket-id/pocket-id/issues/1408
|
||||
if _, err := ldap.ParseDN(member); err != nil {
|
||||
return norm.NFC.String(member)
|
||||
}
|
||||
|
||||
// As a fallback, query LDAP for the referenced entry
|
||||
userSearchReq := ldap.NewSearchRequest(
|
||||
member,
|
||||
|
||||
@@ -141,6 +141,48 @@ func TestLdapServiceSyncAllReconcilesUsersAndGroups(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{"alice", "bob"}, usernames(team.Users))
|
||||
}
|
||||
|
||||
// Regression: posixGroup uses memberUid (bare uid values), not member DNs — issue #1408.
|
||||
func TestLdapServiceSyncAllMapsPosixGroupMemberUid(t *testing.T) {
|
||||
appCfg := defaultTestLDAPAppConfig()
|
||||
appCfg.LdapUserGroupSearchFilter = model.AppConfigVariable{Value: "(objectClass=posixGroup)"}
|
||||
appCfg.LdapAttributeGroupMember = model.AppConfigVariable{Value: "memberUid"}
|
||||
|
||||
service, db := newTestLdapServiceWithAppConfig(t, appCfg, newFakeLDAPClient(
|
||||
ldapSearchResult(
|
||||
ldapEntry("uid=alice,ou=users,dc=example,dc=com", map[string][]string{
|
||||
"entryUUID": {"u-alice"},
|
||||
"uid": {"alice"},
|
||||
"mail": {"alice@example.com"},
|
||||
"givenName": {"Alice"},
|
||||
"sn": {"Jones"},
|
||||
"displayName": {""},
|
||||
}),
|
||||
ldapEntry("uid=bob,ou=users,dc=example,dc=com", map[string][]string{
|
||||
"entryUUID": {"u-bob"},
|
||||
"uid": {"bob"},
|
||||
"mail": {"bob@example.com"},
|
||||
"givenName": {"Bob"},
|
||||
"sn": {"Brown"},
|
||||
"displayName": {""},
|
||||
}),
|
||||
),
|
||||
ldapSearchResult(
|
||||
ldapEntry("cn=users,ou=groups,dc=example,dc=com", map[string][]string{
|
||||
"entryUUID": {"g-users"},
|
||||
"cn": {"users"},
|
||||
"memberUid": {"alice", "bob", "unknown"},
|
||||
}),
|
||||
),
|
||||
))
|
||||
|
||||
require.NoError(t, service.SyncAll(t.Context()))
|
||||
|
||||
var group model.UserGroup
|
||||
require.NoError(t, db.Preload("Users").First(&group, "ldap_id = ?", "g-users").Error)
|
||||
assert.Equal(t, "users", group.Name)
|
||||
assert.ElementsMatch(t, []string{"alice", "bob"}, usernames(group.Users))
|
||||
}
|
||||
|
||||
func TestLdapServiceSyncAllHandlesDuplicateLDAPIDsInSingleRun(t *testing.T) {
|
||||
service, db := newTestLdapService(t, newFakeLDAPClient(
|
||||
ldapSearchResult(
|
||||
@@ -325,7 +367,7 @@ func newFakeLDAPClient(userResult, groupResult *ldap.SearchResult) ldapClient {
|
||||
switch searchRequest.Filter {
|
||||
case "(objectClass=person)":
|
||||
return userResult, nil
|
||||
case "(objectClass=groupOfNames)":
|
||||
case "(objectClass=groupOfNames)", "(objectClass=posixGroup)":
|
||||
return groupResult, nil
|
||||
default:
|
||||
return &ldap.SearchResult{}, nil
|
||||
|
||||
@@ -123,11 +123,9 @@ func (s *OidcService) getJWKCache(ctx context.Context) (*jwk.Cache, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClientRequestDto, userID, ipAddress, userAgent string) (string, string, error) {
|
||||
func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClientRequestDto, userID string, authenticationMethod string, ipAddress, userAgent string) (string, string, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
defer tx.Rollback()
|
||||
|
||||
var client model.OidcClient
|
||||
err := tx.
|
||||
@@ -139,27 +137,49 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if client.RequiresReauthentication {
|
||||
if input.ReauthenticationToken == "" {
|
||||
return "", "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
err = s.webAuthnService.ConsumeReauthenticationToken(ctx, tx, input.ReauthenticationToken, userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// If the client is not public, the code challenge must be provided
|
||||
if client.IsPublic && input.CodeChallenge == "" {
|
||||
return "", "", &common.OidcMissingCodeChallengeError{}
|
||||
}
|
||||
|
||||
// Get the callback URL of the client. Return an error if the provided callback URL is not allowed
|
||||
// Validate the callback URL before any prompt checks, so that prompt-related
|
||||
// error responses never contain an unvalidated redirect target
|
||||
callbackURL, err := s.getCallbackURL(&client, input.CallbackURL, tx, ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Parse prompt parameter (space-delimited list per OIDC spec)
|
||||
promptValues := parsePromptParameter(input.Prompt)
|
||||
hasPromptNone := contains(promptValues, "none")
|
||||
hasPromptLogin := contains(promptValues, "login")
|
||||
hasPromptConsent := contains(promptValues, "consent")
|
||||
hasPromptSelectAccount := contains(promptValues, "select_account")
|
||||
|
||||
// Validate prompt parameter conflicts early.
|
||||
// Per OIDC Core §3.1.2.6, prompt=none must not be combined with any
|
||||
// value that requires user interaction.
|
||||
if hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount) {
|
||||
return "", "", &common.OidcInteractionRequiredError{}
|
||||
}
|
||||
|
||||
// Handle prompt=select_account early (not supported)
|
||||
if hasPromptSelectAccount {
|
||||
return "", "", &common.OidcInteractionRequiredError{}
|
||||
}
|
||||
|
||||
// If prompt=login is specified or the client requires reauthentication, check the reauthentication token
|
||||
if hasPromptLogin || client.RequiresReauthentication {
|
||||
if input.ReauthenticationToken == "" {
|
||||
return "", "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
|
||||
err = s.webAuthnService.ConsumeReauthenticationToken(ctx, tx, input.ReauthenticationToken, userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user group is allowed to authorize the client
|
||||
var user model.User
|
||||
err = tx.
|
||||
@@ -175,13 +195,24 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", &common.OidcAccessDeniedError{}
|
||||
}
|
||||
|
||||
// Handle prompt=none - if consent would be required, we can't show UI
|
||||
if hasPromptNone {
|
||||
hasAlreadyAuthorized, err := s.hasAuthorizedClientInternal(ctx, input.ClientID, userID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !hasAlreadyAuthorized {
|
||||
return "", "", &common.OidcConsentRequiredError{}
|
||||
}
|
||||
}
|
||||
|
||||
hasAlreadyAuthorizedClient, err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Create the authorization code
|
||||
code, err := s.createAuthorizationCode(ctx, input.ClientID, userID, input.Scope, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod, tx)
|
||||
code, err := s.createAuthorizationCode(ctx, input.ClientID, userID, input.Scope, authenticationMethod, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -314,17 +345,17 @@ func (s *OidcService) createTokenFromDeviceCode(ctx context.Context, input dto.O
|
||||
}
|
||||
|
||||
// Explicitly use the input clientID for the audience claim to ensure consistency
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, deviceAuth.Nonce)
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, deviceAuth.Nonce, deviceAuth.AuthenticationMethod)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, *deviceAuth.UserID, deviceAuth.Scope, tx)
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, *deviceAuth.UserID, deviceAuth.Scope, deviceAuth.AuthenticationMethod, tx)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(deviceAuth.User, input.ClientID)
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(deviceAuth.User, input.ClientID, deviceAuth.AuthenticationMethod)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -365,7 +396,7 @@ func (s *OidcService) createTokenFromClientCredentials(ctx context.Context, inpu
|
||||
audClaim = input.Resource
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(dummyUser, audClaim)
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(dummyUser, audClaim, "")
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -413,18 +444,20 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, inpu
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, authorizationCodeMetaData.Nonce)
|
||||
authenticationMethod := authorizationCodeMetaData.AuthenticationMethod
|
||||
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, authorizationCodeMetaData.Nonce, authenticationMethod)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
// Generate a refresh token
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, authorizationCodeMetaData.UserID, authorizationCodeMetaData.Scope, tx)
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, authorizationCodeMetaData.UserID, authorizationCodeMetaData.Scope, authenticationMethod, tx)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(authorizationCodeMetaData.User, input.ClientID)
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(authorizationCodeMetaData.User, input.ClientID, authenticationMethod)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -501,8 +534,46 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, input dto
|
||||
return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
|
||||
}
|
||||
|
||||
if storedRefreshToken.User.Disabled {
|
||||
return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
|
||||
}
|
||||
|
||||
var authorizedClient model.UserAuthorizedOidcClient
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ? AND client_id = ?", storedRefreshToken.UserID, input.ClientID).
|
||||
First(&authorizedClient).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = tx.WithContext(ctx).Delete(&storedRefreshToken).Error
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
|
||||
} else if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
if client.IsGroupRestricted {
|
||||
err = tx.WithContext(ctx).Model(client).Association("AllowedUserGroups").Find(&client.AllowedUserGroups)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if !IsUserGroupAllowedToAuthorize(storedRefreshToken.User, *client) {
|
||||
return CreatedTokens{}, &common.OidcAccessDeniedError{}
|
||||
}
|
||||
|
||||
// Generate a new access token
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(storedRefreshToken.User, input.ClientID)
|
||||
authenticationMethods := storedRefreshToken.AuthenticationMethod
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(storedRefreshToken.User, input.ClientID, authenticationMethods)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -515,13 +586,13 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, input dto
|
||||
|
||||
// Generate a new ID token
|
||||
// There's no nonce here because we don't have one with the refresh token, but that's not required
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, "")
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, "", authenticationMethods)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
// Generate a new refresh token and invalidate the old one
|
||||
newRefreshToken, err := s.createRefreshToken(ctx, input.ClientID, storedRefreshToken.UserID, storedRefreshToken.Scope, tx)
|
||||
newRefreshToken, err := s.createRefreshToken(ctx, input.ClientID, storedRefreshToken.UserID, storedRefreshToken.Scope, authenticationMethods, tx)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -1142,7 +1213,7 @@ func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogo
|
||||
return callbackURL, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID string, userID string, scope string, nonce string, codeChallenge string, codeChallengeMethod string, tx *gorm.DB) (string, error) {
|
||||
func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID string, userID string, scope string, authenticationMethod string, nonce string, codeChallenge string, codeChallengeMethod string, tx *gorm.DB) (string, error) {
|
||||
randomString, err := utils.GenerateRandomAlphanumericString(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1156,6 +1227,7 @@ func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID stri
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
AuthenticationMethod: authenticationMethod,
|
||||
Nonce: nonce,
|
||||
CodeChallenge: &codeChallenge,
|
||||
CodeChallengeMethodSha256: &codeChallengeMethodSha256,
|
||||
@@ -1299,7 +1371,7 @@ func (s *OidcService) CreateDeviceAuthorization(ctx context.Context, input dto.O
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, userID string, ipAddress string, userAgent string) error {
|
||||
func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, userID string, authenticationMethod string, ipAddress string, userAgent string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -1348,6 +1420,7 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
|
||||
}
|
||||
|
||||
deviceAuth.UserID = &userID
|
||||
deviceAuth.AuthenticationMethod = authenticationMethod
|
||||
deviceAuth.IsAuthorized = true
|
||||
|
||||
err = tx.
|
||||
@@ -1464,6 +1537,15 @@ func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string,
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ? AND client_id = ?", userID, clientID).
|
||||
Delete(&model.OidcRefreshToken{}).
|
||||
Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1551,7 +1633,7 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
||||
return dtos, response, err
|
||||
}
|
||||
|
||||
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, tx *gorm.DB) (string, error) {
|
||||
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, authenticationMethod string, tx *gorm.DB) (string, error) {
|
||||
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1562,11 +1644,12 @@ func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, u
|
||||
refreshTokenHash := utils.CreateSha256Hash(refreshToken)
|
||||
|
||||
m := model.OidcRefreshToken{
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(RefreshTokenDuration)),
|
||||
Token: refreshTokenHash,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(RefreshTokenDuration)),
|
||||
Token: refreshTokenHash,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
AuthenticationMethod: authenticationMethod,
|
||||
}
|
||||
|
||||
err = tx.
|
||||
@@ -1782,7 +1865,7 @@ func (s *OidcService) verifyClientAssertionFromFederatedIdentities(ctx context.C
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string) (*dto.OidcClientPreviewDto, error) {
|
||||
func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string, authenticationMethod string) (*dto.OidcClientPreviewDto, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -1818,12 +1901,12 @@ func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, use
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idToken, err := s.jwtService.BuildIDToken(userClaims, clientID, "")
|
||||
idToken, err := s.jwtService.BuildIDToken(userClaims, clientID, "", authenticationMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.BuildOAuthAccessToken(user, clientID)
|
||||
accessToken, err := s.jwtService.BuildOAuthAccessToken(user, clientID, authenticationMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2103,3 +2186,21 @@ func (s *OidcService) GetClientScimServiceProvider(ctx context.Context, clientID
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// parsePromptParameter parses the OIDC prompt parameter which is a space-delimited list of values
|
||||
func parsePromptParameter(prompt string) []string {
|
||||
if prompt == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Fields(prompt)
|
||||
}
|
||||
|
||||
// contains checks if a string slice contains a specific value
|
||||
func contains(slice []string, value string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
@@ -562,6 +563,180 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOidcServiceRefreshTokenAuthorizationState(t *testing.T) {
|
||||
newFixture := func(t *testing.T, isGroupRestricted bool) (*OidcService, *gorm.DB, model.User, model.OidcClient, string, string, *model.UserGroup) {
|
||||
t.Helper()
|
||||
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
common.EnvConfig.EncryptionKey = []byte("0123456789abcdef0123456789abcdef")
|
||||
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
jwtService, err := NewJwtService(t.Context(), db, mockConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
service := &OidcService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
appConfigService: mockConfig,
|
||||
}
|
||||
|
||||
email := "refresh-token-user@example.com"
|
||||
user := model.User{
|
||||
Username: "refresh-token-user",
|
||||
Email: &email,
|
||||
EmailVerified: true,
|
||||
FirstName: "Refresh",
|
||||
LastName: "User",
|
||||
}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
client, err := service.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||
Name: "Refresh Token Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
IsGroupRestricted: isGroupRestricted,
|
||||
},
|
||||
}, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
clientSecret, err := service.CreateClientSecret(t.Context(), client.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var userGroup *model.UserGroup
|
||||
if isGroupRestricted {
|
||||
group := model.UserGroup{
|
||||
FriendlyName: "Allowed Group",
|
||||
Name: "allowed-group",
|
||||
}
|
||||
require.NoError(t, db.Create(&group).Error)
|
||||
require.NoError(t, db.Model(&user).Association("UserGroups").Append(&group))
|
||||
require.NoError(t, db.Model(&client).Association("AllowedUserGroups").Append(&group))
|
||||
userGroup = &group
|
||||
}
|
||||
|
||||
scope := "openid profile email groups"
|
||||
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
||||
UserID: user.ID,
|
||||
ClientID: client.ID,
|
||||
Scope: scope,
|
||||
}).Error)
|
||||
|
||||
refreshToken, err := service.createRefreshToken(t.Context(), client.ID, user.ID, scope, AuthenticationMethodPhishingResistant, db)
|
||||
require.NoError(t, err)
|
||||
|
||||
return service, db, user, client, clientSecret, refreshToken, userGroup
|
||||
}
|
||||
|
||||
refreshInput := func(client model.OidcClient, clientSecret string, refreshToken string) dto.OidcCreateTokensDto {
|
||||
return dto.OidcCreateTokensDto{
|
||||
GrantType: GrantTypeRefreshToken,
|
||||
RefreshToken: refreshToken,
|
||||
ClientID: client.ID,
|
||||
ClientSecret: clientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("rejects refresh token after authorization revocation", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, _ := newFixture(t, false)
|
||||
|
||||
err := service.RevokeAuthorizedClient(t.Context(), user.ID, client.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var refreshTokenCount int64
|
||||
require.NoError(t, db.Model(&model.OidcRefreshToken{}).
|
||||
Where("user_id = ? AND client_id = ?", user.ID, client.ID).
|
||||
Count(&refreshTokenCount).Error)
|
||||
assert.Zero(t, refreshTokenCount)
|
||||
|
||||
_, err = service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcInvalidRefreshTokenError{})
|
||||
})
|
||||
|
||||
t.Run("rejects and deletes stale refresh token without authorization record", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, _ := newFixture(t, false)
|
||||
|
||||
require.NoError(t, db.
|
||||
Where("user_id = ? AND client_id = ?", user.ID, client.ID).
|
||||
Delete(&model.UserAuthorizedOidcClient{}).Error)
|
||||
|
||||
_, err := service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcInvalidRefreshTokenError{})
|
||||
|
||||
var refreshTokenCount int64
|
||||
require.NoError(t, db.Model(&model.OidcRefreshToken{}).
|
||||
Where("user_id = ? AND client_id = ?", user.ID, client.ID).
|
||||
Count(&refreshTokenCount).Error)
|
||||
assert.Zero(t, refreshTokenCount)
|
||||
})
|
||||
|
||||
t.Run("rejects refresh token for disabled user", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, _ := newFixture(t, false)
|
||||
|
||||
require.NoError(t, db.Model(&model.User{}).
|
||||
Where("id = ?", user.ID).
|
||||
Update("disabled", true).Error)
|
||||
|
||||
_, err := service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcInvalidRefreshTokenError{})
|
||||
})
|
||||
|
||||
t.Run("rejects refresh token after user leaves allowed group", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, userGroup := newFixture(t, true)
|
||||
require.NotNil(t, userGroup)
|
||||
|
||||
require.NoError(t, db.Model(&user).Association("UserGroups").Delete(userGroup))
|
||||
|
||||
_, err := service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcAccessDeniedError{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOidcServiceAuthenticationMethodsPersistence(t *testing.T) {
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
jwtService, db, _ := setupJwtService(t, mockConfig)
|
||||
service := &OidcService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
authenticationMethod := AuthenticationMethodPhishingResistant
|
||||
|
||||
t.Run("stores authentication method on authorization codes", func(t *testing.T) {
|
||||
code, err := service.createAuthorizationCode(
|
||||
t.Context(),
|
||||
"amr-client",
|
||||
"amr-user",
|
||||
"openid profile",
|
||||
authenticationMethod,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
db,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var authorizationCode model.OidcAuthorizationCode
|
||||
require.NoError(t, db.First(&authorizationCode, "code = ?", code).Error)
|
||||
assert.Equal(t, authenticationMethod, authorizationCode.AuthenticationMethod)
|
||||
})
|
||||
|
||||
t.Run("stores authentication methods on refresh tokens", func(t *testing.T) {
|
||||
_, err := service.createRefreshToken(t.Context(), "amr-client", "amr-user", "openid profile", authenticationMethod, db)
|
||||
require.NoError(t, err)
|
||||
|
||||
var refreshToken model.OidcRefreshToken
|
||||
require.NoError(t, db.First(&refreshToken, "client_id = ? AND user_id = ?", "amr-client", "amr-user").Error)
|
||||
assert.Equal(t, authenticationMethod, refreshToken.AuthenticationMethod)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateCodeVerifier_Plain(t *testing.T) {
|
||||
require.False(t, validateCodeVerifier("", "", false))
|
||||
require.False(t, validateCodeVerifier("", "", true))
|
||||
@@ -1018,3 +1193,73 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
require.ErrorContains(t, err, "failed to look up client")
|
||||
})
|
||||
}
|
||||
|
||||
// Tests for prompt parameter parsing and handling
|
||||
func TestParsePromptParameter(t *testing.T) {
|
||||
t.Run("empty prompt returns empty slice", func(t *testing.T) {
|
||||
result := parsePromptParameter("")
|
||||
assert.Equal(t, []string{}, result)
|
||||
})
|
||||
|
||||
t.Run("single prompt value", func(t *testing.T) {
|
||||
result := parsePromptParameter("none")
|
||||
assert.Equal(t, []string{"none"}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple prompt values space-delimited", func(t *testing.T) {
|
||||
result := parsePromptParameter("login consent")
|
||||
assert.Equal(t, []string{"login", "consent"}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple prompt values with extra spaces", func(t *testing.T) {
|
||||
result := parsePromptParameter(" none login ")
|
||||
assert.Equal(t, []string{"none", "login"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPromptParameterConflicts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt string
|
||||
expectConflict bool
|
||||
}{
|
||||
{"none alone is valid", "none", false},
|
||||
{"login alone is valid", "login", false},
|
||||
{"consent alone is valid", "consent", false},
|
||||
{"login consent is valid", "login consent", false},
|
||||
{"none consent conflicts", "none consent", true},
|
||||
{"none login conflicts", "none login", true},
|
||||
{"none select_account conflicts", "none select_account", true},
|
||||
{"none consent login conflicts", "none consent login", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
values := parsePromptParameter(tt.prompt)
|
||||
hasNone := contains(values, "none")
|
||||
hasConsent := contains(values, "consent")
|
||||
hasLogin := contains(values, "login")
|
||||
hasSelectAccount := contains(values, "select_account")
|
||||
|
||||
conflict := hasNone && (hasConsent || hasLogin || hasSelectAccount)
|
||||
assert.Equal(t, tt.expectConflict, conflict)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
t.Run("finds value in slice", func(t *testing.T) {
|
||||
slice := []string{"none", "login", "consent"}
|
||||
assert.True(t, contains(slice, "login"))
|
||||
})
|
||||
|
||||
t.Run("returns false for missing value", func(t *testing.T) {
|
||||
slice := []string{"none", "login"}
|
||||
assert.False(t, contains(slice, "consent"))
|
||||
})
|
||||
|
||||
t.Run("returns false for empty slice", func(t *testing.T) {
|
||||
slice := []string{}
|
||||
assert.False(t, contains(slice, "none"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (s *OneTimeAccessService) requestOneTimeAccessEmailInternal(ctx context.Con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We use a background context here as this is running in a goroutine
|
||||
// #nosec G118 - We use a background context here as this is running in a goroutine
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
@@ -197,7 +197,7 @@ func (s *OneTimeAccessService) ExchangeOneTimeAccessToken(ctx context.Context, t
|
||||
return model.User{}, "", &common.DeviceCodeInvalid{}
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(oneTimeAccessToken.User)
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(oneTimeAccessToken.User, AuthenticationMethodOneTimePassword)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
@@ -136,7 +136,8 @@ func (s *UserService) GetProfilePicture(ctx context.Context, userID string) (io.
|
||||
|
||||
// Save the default picture for future use (in a goroutine to avoid blocking)
|
||||
defaultPictureBytes := defaultPicture.Bytes()
|
||||
//nolint:contextcheck
|
||||
//#nosec G118 - We use a background context as this is running in background
|
||||
// nolint:contextcheck
|
||||
go func() {
|
||||
// Use bytes.NewReader because we need an io.ReadSeeker
|
||||
rErr := s.fileStorage.Save(context.Background(), defaultPicturePath, bytes.NewReader(defaultPictureBytes))
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *UserSignUpService) SignUp(ctx context.Context, signupData dto.SignUpDto
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(user)
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(user, "")
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -124,14 +124,12 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var userCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.User{}).
|
||||
Where("id != ?", staticApiKeyUserID).
|
||||
Count(&userCount).Error; err != nil {
|
||||
setupCompleted, err := s.isInitialAdminSetupCompleted(ctx, tx)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
if userCount != 0 {
|
||||
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
||||
if setupCompleted {
|
||||
return model.User{}, "", &common.SetupNotAvailableError{}
|
||||
}
|
||||
|
||||
userToCreate := dto.UserCreateDto{
|
||||
@@ -148,7 +146,7 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
token, err := s.jwtService.GenerateAccessToken(user)
|
||||
token, err := s.jwtService.GenerateAccessToken(user, AuthenticationMethodOneTimePassword)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -161,6 +159,21 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *UserSignUpService) IsInitialAdminSetupCompleted(ctx context.Context) (bool, error) {
|
||||
return s.isInitialAdminSetupCompleted(ctx, s.db)
|
||||
}
|
||||
|
||||
func (s *UserSignUpService) isInitialAdminSetupCompleted(ctx context.Context, db *gorm.DB) (bool, error) {
|
||||
var userCount int64
|
||||
if err := db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("id != ?", staticApiKeyUserID).
|
||||
Count(&userCount).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return userCount != 0, nil
|
||||
}
|
||||
|
||||
func (s *UserSignUpService) 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{})
|
||||
|
||||
@@ -266,7 +266,7 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
|
||||
return model.User{}, "", &common.UserDisabledError{}
|
||||
}
|
||||
|
||||
token, err := s.jwtService.GenerateAccessToken(*user)
|
||||
token, err := s.jwtService.GenerateAccessToken(*user, AuthenticationMethodPhishingResistant)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -293,26 +293,40 @@ func (s *WebAuthnService) ListCredentials(ctx context.Context, userID string) ([
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID string, credentialID string, ipAddress string, userAgent string) error {
|
||||
func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID string, credentialID string, ipAddress string, userAgent string, actorUserID string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
credential := &model.WebauthnCredential{}
|
||||
err := tx.
|
||||
result := tx.
|
||||
WithContext(ctx).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(credential, "id = ? AND user_id = ?", credentialID, userID).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete record: %w", err)
|
||||
Delete(credential, "id = ? AND user_id = ?", credentialID, userID)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to delete record: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
auditLogData := model.AuditLogData{"credentialID": hex.EncodeToString(credential.CredentialID), "passkeyName": credential.Name}
|
||||
if actorUserID != "" && actorUserID != userID {
|
||||
var actor model.User
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
First(&actor, "id = ?", actorUserID).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load actor user: %w", err)
|
||||
}
|
||||
auditLogData["actorUserID"] = actorUserID
|
||||
auditLogData["actorUsername"] = actor.Username
|
||||
}
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventPasskeyRemoved, ipAddress, userAgent, userID, auditLogData, tx)
|
||||
|
||||
err = tx.Commit().Error
|
||||
err := tx.Commit().Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
@@ -375,6 +389,14 @@ func (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(ctx context
|
||||
return "", errors.New("access token does not contain user ID")
|
||||
}
|
||||
|
||||
authenticationMethod, err := GetAuthenticationMethod(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if authenticationMethod != AuthenticationMethodPhishingResistant {
|
||||
return "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
|
||||
// Check if token is issued less than a minute ago
|
||||
tokenExpiration, ok := token.IssuedAt()
|
||||
if !ok || time.Since(tokenExpiration) > time.Minute {
|
||||
|
||||
68
backend/internal/service/webauthn_service_test.go
Normal file
68
backend/internal/service/webauthn_service_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestCreateReauthenticationTokenWithAccessToken(t *testing.T) {
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
|
||||
setupService := func(t *testing.T) (*WebAuthnService, model.User) {
|
||||
t.Helper()
|
||||
|
||||
jwtService, db, _ := setupJwtService(t, mockConfig)
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "reauth-user"},
|
||||
Username: "reauth-user",
|
||||
}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
return &WebAuthnService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
}, user
|
||||
}
|
||||
|
||||
t.Run("accepts a fresh access token from WebAuthn login", func(t *testing.T) {
|
||||
service, user := setupService(t)
|
||||
accessToken, err := service.jwtService.GenerateAccessToken(user, AuthenticationMethodPhishingResistant)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, reauthenticationToken)
|
||||
})
|
||||
|
||||
t.Run("rejects a fresh access token from one-time access login", func(t *testing.T) {
|
||||
service, user := setupService(t)
|
||||
accessToken, err := service.jwtService.GenerateAccessToken(user, AuthenticationMethodOneTimePassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
assert.Empty(t, reauthenticationToken)
|
||||
require.Error(t, err)
|
||||
assert.ErrorAs(t, err, new(*common.ReauthenticationRequiredError))
|
||||
})
|
||||
|
||||
t.Run("rejects a fresh access token without an authentication method", func(t *testing.T) {
|
||||
service, user := setupService(t)
|
||||
accessToken, err := service.jwtService.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
assert.Empty(t, reauthenticationToken)
|
||||
require.Error(t, err)
|
||||
assert.ErrorAs(t, err, new(*common.ReauthenticationRequiredError))
|
||||
})
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error)
|
||||
continue
|
||||
}
|
||||
objects = append(objects, ObjectInfo{
|
||||
Path: aws.ToString(obj.Key),
|
||||
Path: s.pathFromKey(aws.ToString(obj.Key)),
|
||||
Size: aws.ToInt64(obj.Size),
|
||||
ModTime: aws.ToTime(obj.LastModified),
|
||||
})
|
||||
@@ -147,6 +147,13 @@ func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error)
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func (s *s3Storage) pathFromKey(key string) string {
|
||||
if s.prefix == "" {
|
||||
return key
|
||||
}
|
||||
return strings.TrimPrefix(key, s.prefix+"/")
|
||||
}
|
||||
|
||||
func (s *s3Storage) Walk(ctx context.Context, root string, fn func(ObjectInfo) error) error {
|
||||
objects, err := s.List(ctx, root)
|
||||
if err != nil {
|
||||
|
||||
@@ -35,6 +35,50 @@ func TestS3Helpers(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pathFromKey strips prefix to honor relative-path contract", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{name: "no prefix returns key unchanged", prefix: "", key: "images/logo.png", expected: "images/logo.png"},
|
||||
{name: "no prefix empty key", prefix: "", key: "", expected: ""},
|
||||
{name: "prefix matches and is stripped", prefix: "data/uploads", key: "data/uploads/application-images/logo.svg", expected: "application-images/logo.svg"},
|
||||
{name: "single-segment prefix stripped", prefix: "root", key: "root/foo/bar.txt", expected: "foo/bar.txt"},
|
||||
{name: "prefix equal to key without trailing slash is unchanged", prefix: "root", key: "root", expected: "root"},
|
||||
{name: "key without expected prefix returned unchanged", prefix: "data/uploads", key: "other/path.txt", expected: "other/path.txt"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := &s3Storage{
|
||||
bucket: "bucket",
|
||||
prefix: tc.prefix,
|
||||
}
|
||||
assert.Equal(t, tc.expected, s.pathFromKey(tc.key))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pathFromKey is the inverse of buildObjectKey for clean paths", func(t *testing.T) {
|
||||
paths := []string{
|
||||
"images/logo.png",
|
||||
"application-images/logo.svg",
|
||||
"oidc-client-images/abc.png",
|
||||
"deeply/nested/file.bin",
|
||||
}
|
||||
prefixes := []string{"", "root", "data/uploads"}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
for _, p := range paths {
|
||||
s := &s3Storage{bucket: "bucket", prefix: prefix}
|
||||
assert.Equal(t, p, s.pathFromKey(s.buildObjectKey(p)),
|
||||
"round-trip failed for prefix=%q path=%q", prefix, p)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("isS3NotFound detects expected errors", func(t *testing.T) {
|
||||
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NoSuchKey"}))
|
||||
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NotFound"}))
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestValidateCallbackURLPattern(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "wildcard userinfo",
|
||||
pattern: "https://user:*@example.com/callback",
|
||||
pattern: "https://user:*@example.com/callback", // #nosec G101 - Test credential
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -64,6 +64,7 @@ func TestBearerAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// #nosec G101 - Test credentials
|
||||
func TestOAuthClientBasicAuth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE oidc_authorization_codes DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_refresh_tokens DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_device_codes DROP COLUMN authentication_method;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_device_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE oidc_authorization_codes DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_refresh_tokens DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_device_codes DROP COLUMN authentication_method;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_device_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
@@ -40,7 +40,7 @@ RUN VERSION=$(cat /build/.version) \
|
||||
.
|
||||
|
||||
# Stage 3: Production Image
|
||||
FROM alpine
|
||||
FROM alpine:3.23.4
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl su-exec
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This Dockerfile embeds a pre-built binary for the given Linux architecture
|
||||
# Binaries must be built using "./scripts/development/build-binaries.sh --docker-only"
|
||||
|
||||
FROM alpine
|
||||
FROM alpine:3.23.4
|
||||
|
||||
# TARGETARCH can be "amd64" or "arm64"
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "1.0.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-email/preview-server": "5.0.7",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-email": "5.0.7",
|
||||
"tsx": "^4.21.0"
|
||||
|
||||
528
frontend/messages/ca.json
Normal file
528
frontend/messages/ca.json
Normal file
@@ -0,0 +1,528 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "El meu compte",
|
||||
"logout": "Tancar la sessió",
|
||||
"confirm": "Confirma",
|
||||
"docs": "Documentació",
|
||||
"key": "Clau",
|
||||
"value": "Valor",
|
||||
"remove_custom_claim": "Elimina l'atribut personalitzat",
|
||||
"add_custom_claim": "Afegeix un atribut personalitzat",
|
||||
"add_another": "Afegeix-ne una altra",
|
||||
"select_a_date": "Selecciona una data",
|
||||
"select_file": "Selecciona un fitxer",
|
||||
"profile_picture": "Imatge de perfil",
|
||||
"profile_picture_is_managed_by_ldap_server": "La imatge de perfil és gestionada pel servidor LDAP i no es pot canviar aquí.",
|
||||
"click_profile_picture_to_upload_custom": "Fes clic a la imatge de perfil per pujar-ne una des dels teus fitxers.",
|
||||
"image_should_be_in_format": "La imatge ha d'estar en format PNG, JPEG o WEBP.",
|
||||
"items_per_page": "Elements per pàgina",
|
||||
"no_items_found": "No s'han trobat elements",
|
||||
"select_items": "Selecciona elements...",
|
||||
"search": "Cerca...",
|
||||
"expand_card": "Expandeix la targeta",
|
||||
"copied": "Copiat",
|
||||
"click_to_copy": "Fes clic per copiar",
|
||||
"something_went_wrong": "Alguna cosa ha fallat",
|
||||
"go_back_to_home": "Torna a la pàgina principal",
|
||||
"alternative_sign_in_methods": "Mètodes alternatius d'inici de sessió",
|
||||
"login_background": "Fons d'inici de sessió",
|
||||
"logo": "Logotip",
|
||||
"login_code": "Codi d'inici de sessió",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Crea un codi d'inici de sessió d'un sol ús que l'usuari pot utilitzar per accedir sense clau d'accés.",
|
||||
"one_hour": "1 hora",
|
||||
"twelve_hours": "12 hores",
|
||||
"one_day": "1 dia",
|
||||
"one_week": "1 setmana",
|
||||
"one_month": "1 mes",
|
||||
"expiration": "Expiració",
|
||||
"generate_code": "Genera Codi",
|
||||
"name": "Nom",
|
||||
"browser_unsupported": "Navegador no compatible",
|
||||
"this_browser_does_not_support_passkeys": "Aquest navegador no admet claus d'accés. Utilitza un mètode alternatiu per iniciar la sessió.",
|
||||
"an_unknown_error_occurred": "S'ha produït un error desconegut",
|
||||
"authentication_process_was_aborted": "El procés d'autenticació s'ha cancel·lat",
|
||||
"error_occurred_with_authenticator": "S'ha produït un error amb l'autenticador",
|
||||
"authenticator_does_not_support_discoverable_credentials": "L'autenticador no admet credencials descobribles",
|
||||
"authenticator_does_not_support_resident_keys": "L'autenticador no admet claus residents",
|
||||
"passkey_was_previously_registered": "Aquesta clau d'accés ja estava registrada",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "L'autenticador no admet cap dels algoritmes sol·licitats",
|
||||
"webauthn_error_invalid_rp_id": "L'identificador de relying party configurat no és vàlid.",
|
||||
"webauthn_error_invalid_domain": "El domini configurat no és vàlid.",
|
||||
"contact_administrator_to_fix": "Contacta amb l'administrador per solucionar-ho.",
|
||||
"webauthn_operation_not_allowed_or_timed_out": "L'operació no està permesa o ha expirat",
|
||||
"webauthn_not_supported_by_browser": "Les claus d'accés no són compatibles amb aquest navegador. Utilitza un mètode alternatiu per iniciar la sessió.",
|
||||
"critical_error_occurred_contact_administrator": "S'ha produït un error crític. Contacta amb l'administrador.",
|
||||
"sign_in_to": "Inicia la sessió a {name}",
|
||||
"client_not_found": "Client no trobat",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> vol accedir a la informació següent:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Vols iniciar la sessió a <b>{client}</b> amb el teu compte de {appName}?",
|
||||
"email": "Correu electrònic",
|
||||
"view_your_email_address": "Mostra la teva adreça de correu",
|
||||
"profile": "Perfil",
|
||||
"view_your_profile_information": "Mostra la informació del teu perfil",
|
||||
"groups": "Grups",
|
||||
"view_the_groups_you_are_a_member_of": "Mostra els grups dels quals ets membre",
|
||||
"cancel": "Cancel·la",
|
||||
"sign_in": "Inicia la sessió",
|
||||
"try_again": "Torna-ho a intentar",
|
||||
"client_logo": "Logotip del client",
|
||||
"sign_out": "Tanca sessió",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Vols tancar la sessió de {appName} amb el compte <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Inicia la sessió a {appName}",
|
||||
"please_try_to_sign_in_again": "Si us plau, torna a iniciar la sessió.",
|
||||
"authenticate_with_passkey_to_access_account": "Autentica't amb la teva clau d'accés per accedir al teu compte.",
|
||||
"authenticate": "Autentica't",
|
||||
"please_try_again": "Si us plau, torna-ho a intentar.",
|
||||
"continue": "Continua",
|
||||
"alternative_sign_in": "Inici de sessió alternatiu",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Si no tens accés a la teva clau d'accés, pots iniciar la sessió amb un dels mètodes següents.",
|
||||
"use_your_passkey_instead": "Altrament, vols fer servir la teva clau d'accés?",
|
||||
"email_login": "Inici de sessió per correu",
|
||||
"enter_a_login_code_to_sign_in": "Introdueix un codi d'inici de sessió per iniciar la sessió.",
|
||||
"sign_in_with_login_code": "Inicia la sessió amb un codi",
|
||||
"request_a_login_code_via_email": "Sol·licita un codi d'inici de sessió per correu.",
|
||||
"go_back": "Torna enrere",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "S'ha enviat un correu a l'adreça proporcionada, si existeix al sistema.",
|
||||
"enter_code": "Introdueix el codi",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Introdueix la teva adreça de correu per rebre un codi d'inici de sessió.",
|
||||
"your_email": "El teu correu",
|
||||
"submit": "Envia",
|
||||
"enter_the_code_you_received_to_sign_in": "Introdueix el codi que has rebut per iniciar la sessió.",
|
||||
"code": "Codi",
|
||||
"invalid_redirect_url": "URL de redirecció invàlida",
|
||||
"audit_log": "Registre d'auditoria",
|
||||
"users": "Usuaris",
|
||||
"user_groups": "Grups d'usuaris",
|
||||
"oidc_clients": "Clients OIDC",
|
||||
"api_keys": "Claus API",
|
||||
"application_configuration": "Configuració de l'aplicació",
|
||||
"settings": "Configuració",
|
||||
"update_pocket_id": "Actualitza Pocket ID",
|
||||
"powered_by": "Impulsat per",
|
||||
"see_your_recent_account_activities": "Veuràs les activitats del teu compte dins del període de retenció configurat.",
|
||||
"time": "Hora",
|
||||
"event": "Esdeveniment",
|
||||
"approximate_location": "Ubicació aproximada",
|
||||
"ip_address": "Adreça IP",
|
||||
"device": "Dispositiu",
|
||||
"client": "Client",
|
||||
"actor": "Actor",
|
||||
"unknown": "Desconegut",
|
||||
"account_details_updated_successfully": "Els detalls del compte s'han actualitzat correctament",
|
||||
"profile_picture_updated_successfully": "La imatge de perfil s'ha actualitzat correctament. Pot trigar uns minuts a actualitzar-se.",
|
||||
"account_settings": "Configuració del compte",
|
||||
"passkey_missing": "Clau d'accés absent",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Si us plau, afegeix una clau d'accés per evitar perdre l'accés al teu compte.",
|
||||
"single_passkey_configured": "Clau d'accés única configurada",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Es recomana afegir més d'una clau d'accés per evitar quedar sense accés al compte.",
|
||||
"account_details": "Detalls del compte",
|
||||
"passkeys": "Clau d'accéss",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gestiona les claus d'accés que pots utilitzar per autenticar-te.",
|
||||
"manage_this_users_passkeys": "Gestiona les claus d'accés d'aquest usuari.",
|
||||
"add_passkey": "Afegeix clau d'accés",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Crea un codi d'un sol ús per iniciar la sessió des d'un altre dispositiu sense clau d'accés.",
|
||||
"create": "Crea",
|
||||
"first_name": "Nom",
|
||||
"last_name": "Cognom",
|
||||
"username": "Nom d'usuari",
|
||||
"save": "Desa",
|
||||
"username_can_only_contain": "El nom d'usuari només pot contenir lletres minúscules, números, guions baixos, punts, guions i el símbol '@'",
|
||||
"username_must_start_with": "El nom d'usuari ha de començar per un caràcter alfanumèric",
|
||||
"username_must_end_with": "El nom d'usuari ha de acabar amb un caràcter alfanumèric",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Inicia la sessió amb el codi següent. El codi caducarà en 15 minuts.",
|
||||
"or_visit": "o visita",
|
||||
"added_on": "Afegit el",
|
||||
"rename": "Reanomena",
|
||||
"delete": "Elimina",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Segur que vols eliminar aquesta clau d'accés?",
|
||||
"passkey_deleted_successfully": "Clau d'accés eliminada correctament",
|
||||
"delete_passkey_name": "Elimina {passkeyName}",
|
||||
"passkey_name_updated_successfully": "S'ha actualitzat correctament el nom de la clau d'accés",
|
||||
"name_passkey": "Anomena la clau d'accés",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Dóna un nom a la teva clau d'accés per identificar-la fàcilment més endavant.",
|
||||
"create_api_key": "Crea una clau API",
|
||||
"add_a_new_api_key_for_programmatic_access": "Afegeix una nova clau API per a l'accés programàtic a la <link href='https://pocket-id.org/docs/api'>API de Pocket ID</link>.",
|
||||
"add_api_key": "Afegeix una clau API",
|
||||
"manage_api_keys": "Gestiona les claus API",
|
||||
"api_key_created": "S'ha creat una Clau API",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Per motius de seguretat aquesta clau només es mostrarà una vegada. Desa-la de manera segura.",
|
||||
"description": "Descripció",
|
||||
"api_key": "Clau API",
|
||||
"close": "Tanca",
|
||||
"name_to_identify_this_api_key": "Nom per identificar aquesta clau API.",
|
||||
"expires_at": "Caduca el",
|
||||
"when_this_api_key_will_expire": "Quan caducarà aquesta clau API.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Descripció opcional per ajudar a identificar l'ús d'aquesta clau.",
|
||||
"expiration_date_must_be_in_the_future": "La data d'expiració ha de ser en el futur",
|
||||
"revoke_api_key": "Revoca la clau API",
|
||||
"never": "Mai",
|
||||
"revoke": "Revoca",
|
||||
"api_key_revoked_successfully": "La clau API ha estat revocada correctament",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Segur que vols revocar la clau API \"{apiKeyName}\"? Això trencarà qualsevol integració que utilitzi aquesta clau.",
|
||||
"last_used": "Últim ús",
|
||||
"actions": "Accions",
|
||||
"images_updated_successfully": "Imatges actualitzades correctament. Pot trigar uns minuts a actualitzar-se.",
|
||||
"general": "General",
|
||||
"configure_smtp_to_send_emails": "Configura SMTP per enviar notificacions per correu quan hi hagi un inici de sessió des d'un dispositiu o ubicació nova.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configura LDAP per sincronitzar usuaris i grups des d'un servidor LDAP.",
|
||||
"images": "Imatges",
|
||||
"update": "Actualitza",
|
||||
"email_configuration_updated_successfully": "Configuració de correu actualitzada correctament",
|
||||
"save_changes_question": "Vols desar els canvis?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Has de desar els canvis abans d'enviar un correu de prova. Vols desar-los ara?",
|
||||
"save_and_send": "Desa i envia",
|
||||
"test_email_sent_successfully": "El correu de prova ha estat enviat correctament a la teva adreça.",
|
||||
"failed_to_send_test_email": "No s'ha pogut enviar el correu de prova. Consulta els registres d'activitat del servidor per més informació.",
|
||||
"smtp_configuration": "Configuració SMTP",
|
||||
"smtp_host": "Servidor SMTP",
|
||||
"smtp_port": "Port SMTP",
|
||||
"smtp_user": "Usuari SMTP",
|
||||
"smtp_password": "Contrasenya SMTP",
|
||||
"smtp_from": "Remitent SMTP",
|
||||
"smtp_tls_option": "Opció TLS SMTP",
|
||||
"email_tls_option": "Opció TLS del correu",
|
||||
"skip_certificate_verification": "Omet la verificació del certificat",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Això pot ser útil per certificats autosignats.",
|
||||
"enabled_emails": "Correus habilitats",
|
||||
"email_login_notification": "Notificació d'inici de sessió per correu",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Envia un correu a l'usuari quan iniciï la sessió des d'un dispositiu nou.",
|
||||
"emai_login_code_requested_by_user": "Codi d'inici de sessió sol·licitat per correu",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permet als usuaris iniciar la sessió amb un codi enviat al seu correu. Això redueix significativament la seguretat, ja que qualsevol amb accés al correu de l'usuari podria accedir.",
|
||||
"email_login_code_from_admin": "Codi d'inici de sessió per correu des d'administrador",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Permet que un administrador enviï un codi d'inici de sessió a l'usuari per correu.",
|
||||
"send_test_email": "Envia un correu de prova",
|
||||
"application_configuration_updated_successfully": "Configuració de l'aplicació actualitzada correctament",
|
||||
"application_name": "Nom de l'aplicació",
|
||||
"session_duration": "Durada de la sessió",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Durada d'una sessió en minuts abans que l'usuari hagi d'iniciar la sessió de nou.",
|
||||
"enable_self_account_editing": "Permet editar el propi compte",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Permet als usuaris editar els seus propis detalls del compte.",
|
||||
"ldap_configuration_updated_successfully": "Configuració LDAP actualitzada correctament",
|
||||
"ldap_disabled_successfully": "LDAP deshabilitat correctament",
|
||||
"ldap_sync_finished": "Sincronització LDAP finalitzada",
|
||||
"client_configuration": "Configuració del client",
|
||||
"ldap_url": "URL LDAP",
|
||||
"ldap_bind_dn": "DN de vinculació LDAP",
|
||||
"ldap_bind_password": "Contrasenya de vinculació LDAP",
|
||||
"ldap_base_dn": "Base DN LDAP",
|
||||
"user_search_filter": "Filtre de cerca d'usuaris",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "El filtre de cerca per utilitzar per buscar/sincronitzar usuaris.",
|
||||
"groups_search_filter": "Filtre de cerca de grups",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "El filtre de cerca per utilitzar per buscar/sincronitzar grups.",
|
||||
"attribute_mapping": "Mapeig d'atributs",
|
||||
"user_unique_identifier_attribute": "Atribut identificador únic d'usuari",
|
||||
"the_value_of_this_attribute_should_never_change": "El valor d'aquest atribut no hauria de canviar mai.",
|
||||
"username_attribute": "Atribut de nom d'usuari",
|
||||
"user_mail_attribute": "Atribut de correu de l'usuari",
|
||||
"user_first_name_attribute": "Atribut de nom de l'usuari",
|
||||
"user_last_name_attribute": "Atribut de cognom de l'usuari",
|
||||
"user_profile_picture_attribute": "Atribut de la imatge de perfil de l'usuari",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "El valor d'aquest atribut pot ser una URL, un binari o una imatge codificada en base64.",
|
||||
"group_members_attribute": "Atribut de membres del grup",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "L'atribut a utilitzar per consultar els membres d'un grup.",
|
||||
"group_unique_identifier_attribute": "Atribut identificador únic del grup",
|
||||
"group_rdn_attribute": "Atribut RDN del grup (en DN)",
|
||||
"admin_group_name": "Nom del grup d'administradors",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Els membres d'aquest grup tindran privilegis d'administrador a Pocket ID.",
|
||||
"disable": "Desactiva",
|
||||
"sync_now": "Sincronitza ara",
|
||||
"enable": "Activa",
|
||||
"user_created_successfully": "Usuari creat correctament",
|
||||
"create_user": "Crea usuari",
|
||||
"add_a_new_user_to_appname": "Afegeix un nou usuari a {appName}",
|
||||
"add_user": "Afegeix usuari",
|
||||
"manage_users": "Gestiona usuaris",
|
||||
"admin_privileges": "Privilegis d'administrador",
|
||||
"admins_have_full_access_to_the_admin_panel": "Els administradors tenen accés complet al tauler d'administració.",
|
||||
"delete_firstname_lastname": "Elimina {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Segur que vols eliminar aquest usuari?",
|
||||
"user_deleted_successfully": "Usuari eliminat correctament",
|
||||
"role": "Rol",
|
||||
"source": "Origen",
|
||||
"admin": "Administrador",
|
||||
"user": "Usuari",
|
||||
"local": "Local",
|
||||
"toggle_menu": "Mostra/Amaga menú",
|
||||
"edit": "Edita",
|
||||
"user_groups_updated_successfully": "Grups d'usuaris actualitzats correctament",
|
||||
"user_updated_successfully": "Usuari actualitzat correctament",
|
||||
"custom_claims_updated_successfully": "Atributs personalitzats actualitzades correctament",
|
||||
"back": "Enrere",
|
||||
"user_details_firstname_lastname": "Detalls de l'usuari {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Gestiona a quins grups pertany aquest usuari.",
|
||||
"custom_claims": "Atributs personalitzats",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Els atributs personalitzats són parells clau-valor que permeten emmagatzemar informació addicional sobre un usuari. Aquestes afirmacions s'inclouran en el token ID si es sol·licita l'abast 'profile'.",
|
||||
"user_group_created_successfully": "Grup d'usuaris creat correctament",
|
||||
"create_user_group": "Crea grup d'usuaris",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Crea un nou grup que es pot assignar als usuaris.",
|
||||
"add_group": "Afegeix grup",
|
||||
"manage_user_groups": "Gestiona grups d'usuaris",
|
||||
"friendly_name": "Nom amigable",
|
||||
"name_that_will_be_displayed_in_the_ui": "Nom que es mostrarà a la interfície",
|
||||
"name_that_will_be_in_the_groups_claim": "Nom que apareixerà a l'atribut \"groups\"",
|
||||
"delete_name": "Elimina {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Segur que vols eliminar aquest grup d'usuaris?",
|
||||
"user_group_deleted_successfully": "Grup d'usuaris eliminat correctament",
|
||||
"user_count": "Nombre d'usuaris",
|
||||
"user_group_updated_successfully": "Grup d'usuaris actualitzat correctament",
|
||||
"users_updated_successfully": "Usuaris actualitzats correctament",
|
||||
"user_group_details_name": "Detalls del grup d'usuaris {name}",
|
||||
"assign_users_to_this_group": "Assigna usuaris a aquest grup.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Els atributs personalitzats són parells clau-valor que permeten emmagatzemar informació addicional sobre un usuari. Aquestes afirmacions s'inclouran en el token ID si es sol·licita l'abast 'profile'. Les afirmacions definides a l'usuari tindran prioritat en cas de conflicte.",
|
||||
"oidc_client_created_successfully": "Client OIDC creat correctament",
|
||||
"create_oidc_client": "Crea client OIDC",
|
||||
"add_a_new_oidc_client_to_appname": "Afegeix un nou client OIDC a {appName}.",
|
||||
"add_oidc_client": "Afegeix client OIDC",
|
||||
"manage_oidc_clients": "Gestiona clients OIDC",
|
||||
"one_time_link": "Enllaç d'una sola vegada",
|
||||
"use_this_link_to_sign_in_once": "Utilitza aquest enllaç per iniciar la sessió una sola vegada. És necessari per a usuaris que no han afegit una clau d'accés o l'han perduda.",
|
||||
"add": "Afegeix",
|
||||
"callback_urls": "URLs de callback",
|
||||
"logout_callback_urls": "URLs de tancar la sessió",
|
||||
"public_client": "Client públic",
|
||||
"public_clients_description": "Els clients públics no tenen secret de client. Estan dissenyats per a aplicacions mòbils, web i natives on els secrets no es poden emmagatzemar de forma segura.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange és una característica de seguretat per prevenir atacs CSRF i la interceptació del codi d'autorització.",
|
||||
"requires_reauthentication": "Requereix re-autenticació",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Requereix que els usuaris s'autentiquin de nou en cada autorització, encara que ja estiguin connectats",
|
||||
"name_logo": "Logotip de {name}",
|
||||
"change_logo": "Canvia el logotip",
|
||||
"upload_logo": "Puja logotip",
|
||||
"remove_logo": "Elimina logotip",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Estàs segur que vols esborrar aquest client OIDC?",
|
||||
"oidc_client_deleted_successfully": "Client OIDC eliminat correctament",
|
||||
"authorization_url": "URL d'autorització",
|
||||
"oidc_discovery_url": "URL de descobriment OIDC",
|
||||
"token_url": "URL del token",
|
||||
"userinfo_url": "URL d'informació de l'usuari",
|
||||
"logout_url": "URL de tancar la sessió",
|
||||
"certificate_url": "URL del certificat",
|
||||
"enabled": "Activat",
|
||||
"disabled": "Desactivat",
|
||||
"oidc_client_updated_successfully": "Client OIDC actualitzat correctament",
|
||||
"create_new_client_secret": "Crea un nou secret de client",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Segur que vols crear un nou secret de client? L'anterior quedarà invalidat.",
|
||||
"generate": "Genera",
|
||||
"new_client_secret_created_successfully": "Nou secret de client creat correctament",
|
||||
"oidc_client_name": "Client OIDC {name}",
|
||||
"client_id": "ID del client",
|
||||
"client_secret": "Secret del client",
|
||||
"show_more_details": "Mostra més detalls",
|
||||
"allowed_user_groups": "Grups d'usuaris permesos",
|
||||
"allowed_user_groups_description": "Selecciona els grups d'usuaris els membres dels quals poden iniciar la sessió a aquest client.",
|
||||
"allowed_user_groups_status_unrestricted_description": "No s'apliquen restriccions de grup. Qualsevol usuari pot iniciar la sessió a aquest client.",
|
||||
"unrestrict": "Elimina restriccions",
|
||||
"restrict": "Restringeix",
|
||||
"user_groups_restriction_updated_successfully": "Restricció de grups d'usuaris actualitzada correctament",
|
||||
"allowed_user_groups_updated_successfully": "Grups d'usuaris permesos actualitzats correctament",
|
||||
"favicon": "Icona de la web",
|
||||
"light_mode_logo": "Logotip per mode clar",
|
||||
"dark_mode_logo": "Logotip per mode fosc",
|
||||
"email_logo": "Logotip per correus",
|
||||
"background_image": "Imatge de fons",
|
||||
"language": "Idioma",
|
||||
"reset_profile_picture_question": "Restablir la imatge de perfil?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Això eliminarà la imatge pujada i restablirà la imatge de perfil per defecte. Vols continuar?",
|
||||
"reset": "Restableix",
|
||||
"reset_to_default": "Restableix per defecte",
|
||||
"profile_picture_has_been_reset": "La imatge de perfil s'ha restablert. Pot trigar uns minuts a actualitzar-se.",
|
||||
"select_the_language_you_want_to_use": "Selecciona l'idioma que vols utilitzar. Tingues en compte que algun text podria haver estat traduït automàticament i ser inexacte.",
|
||||
"contribute_to_translation": "Si trobes un error, ets benvingut a contribuir a la traducció a <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Personal",
|
||||
"global": "Global",
|
||||
"all_users": "Tots els usuaris",
|
||||
"all_events": "Tots els esdeveniments",
|
||||
"all_clients": "Tots els clients",
|
||||
"all_locations": "Totes les ubicacions",
|
||||
"global_audit_log": "Registre d'auditoria global",
|
||||
"see_all_recent_account_activities": "Mostra les activitats dels comptes de tots els usuaris durant el període de retenció establert.",
|
||||
"token_sign_in": "Inici de sessió amb token",
|
||||
"client_authorization": "Autorizació del client",
|
||||
"new_client_authorization": "Nova autorització de client",
|
||||
"device_code_authorization": "Autorizació per codi de dispositiu",
|
||||
"new_device_code_authorization": "Nova autorització per codi de dispositiu",
|
||||
"passkey_added": "Clau d'accés afegida",
|
||||
"passkey_removed": "Clau d'accés eliminada",
|
||||
"disable_animations": "Desactiva animacions",
|
||||
"turn_off_ui_animations": "Desactiva les animacions de la interfície d'usuari.",
|
||||
"user_disabled": "Compte desactivat",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Els usuaris desactivats no poden iniciar la sessió ni utilitzar serveis.",
|
||||
"user_disabled_successfully": "Usuari desactivat correctament.",
|
||||
"user_enabled_successfully": "Usuari activat correctament.",
|
||||
"status": "Estat",
|
||||
"disable_firstname_lastname": "Desactiva {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Segur que vols desactivar aquest usuari? No podrà iniciar la sessió ni accedir als serveis.",
|
||||
"ldap_soft_delete_users": "Mantenir usuaris desactivats des de LDAP.",
|
||||
"ldap_soft_delete_users_description": "Quan està habilitat, els usuaris eliminats de LDAP seran desactivats en lloc d'eliminats del sistema.",
|
||||
"login_code_email_success": "El codi d'inici de sessió s'ha enviat a l'usuari.",
|
||||
"send_email": "Envia correu",
|
||||
"show_code": "Mostra codi",
|
||||
"callback_url_description": "URL(s) proporcionada(s) pel teu client. S'afegiran automàticament si es deixa en blanc. S'admeten <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>comodins</link>.",
|
||||
"logout_callback_url_description": "URL(s) proporcionada(s) pel teu client per tancar la sessió. S'admeten <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>comodins</link>.",
|
||||
"api_key_expiration": "Caducitat de la clau API",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envia un correu a l'usuari quan la seva clau API estigui a punt de caducar.",
|
||||
"authorize_device": "Autoritza dispositiu",
|
||||
"the_device_has_been_authorized": "El dispositiu ha estat autoritzat.",
|
||||
"enter_code_displayed_in_previous_step": "Introdueix el codi que es va mostrar al pas anterior.",
|
||||
"authorize": "Autoritza",
|
||||
"federated_client_credentials": "Credencials de client federat",
|
||||
"federated_client_credentials_description": "Les credencials de client federat permeten autenticar clients OIDC sense gestionar secrets de llarga durada. Fan servir tokens JWT emesos per autoritats de tercers per a assertions de client, p. ex. tokens d'identitat de càrrega de treball.",
|
||||
"add_federated_client_credential": "Afegeix credencial de client federat",
|
||||
"add_another_federated_client_credential": "Afegeix una altra credencial de client federat",
|
||||
"oidc_allowed_group_count": "Nombre de grups permesos",
|
||||
"unrestricted": "Sense restriccions",
|
||||
"show_advanced_options": "Mostra opcions avançades",
|
||||
"hide_advanced_options": "Oculta opcions avançades",
|
||||
"oidc_data_preview": "Previsualització de dades OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Previsualitza les dades OIDC que s'enviarien per a diferents usuaris",
|
||||
"id_token": "Token ID",
|
||||
"access_token": "Token d'accés",
|
||||
"userinfo": "Informació d'usuari",
|
||||
"id_token_payload": "Contingut del token ID",
|
||||
"access_token_payload": "Contingut del token d'accés",
|
||||
"userinfo_endpoint_response": "Resposta de l'endpoint userinfo",
|
||||
"copy": "Copia",
|
||||
"no_preview_data_available": "No hi ha dades de previsualització disponibles",
|
||||
"copy_all": "Copia-ho tot",
|
||||
"preview": "Previsualitza",
|
||||
"preview_for_user": "Previsualitza per a {name}",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Previsualitza les dades OIDC que s'enviarien per a aquest usuari",
|
||||
"show": "Mostra",
|
||||
"select_an_option": "Selecciona una opció",
|
||||
"select_user": "Selecciona usuari",
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Selecciona un color d'èmfasi per personalitzar l'aparença de Pocket ID.",
|
||||
"accent_color": "Color d'èmfasi",
|
||||
"custom_accent_color": "Color d'èmfasi personalitzat",
|
||||
"custom_accent_color_description": "Introdueix un color personalitzat utilitzant formats CSS vàlids (p.ex., hex, rgb, hsl).",
|
||||
"color_value": "Valor del color",
|
||||
"apply": "Aplica",
|
||||
"signup_token": "Token d'alta",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "Crea un token d'alta per permetre l'alta de nous usuaris.",
|
||||
"usage_limit": "Límit d'ús",
|
||||
"number_of_times_token_can_be_used": "Nombre de vegades que es pot utilitzar el token d'alta.",
|
||||
"expires": "Caduca",
|
||||
"signup": "Dona't d'alta",
|
||||
"user_creation": "Creació d'usuari",
|
||||
"configure_user_creation": "Gestiona la creació d'usuaris, incloent mètodes de d'alta i permisos per defecte.",
|
||||
"user_creation_groups_description": "Assigna aquests grups automàticament quan es donin d'alta nous usuaris.",
|
||||
"user_creation_claims_description": "Assigna aquests atributs personalitzats automàticament quan es donin d'alta nous usuaris.",
|
||||
"user_creation_updated_successfully": "Configuració de creació d'usuaris actualitzada correctament.",
|
||||
"signup_disabled_description": "L'alta d'usuaris està deshabilitat. Només els administradors poden crear comptes nous.",
|
||||
"signup_requires_valid_token": "Es requereix un token d'alta vàlid per crear un compte",
|
||||
"validating_signup_token": "Validant el token d'alta",
|
||||
"go_to_login": "Vés a iniciar la sessió",
|
||||
"signup_to_appname": "Dona't d'alta a {appName}",
|
||||
"create_your_account_to_get_started": "Crea el teu compte per començar.",
|
||||
"initial_account_creation_description": "Si us plau, crea el teu compte per començar. Podràs configurar una clau d'accés més endavant.",
|
||||
"setup_your_passkey": "Configura la teva clau d'accés",
|
||||
"create_a_passkey_to_securely_access_your_account": "Crea una clau d'accés per accedir de manera segura al teu compte. Aquesta serà la teva manera principal d'iniciar la sessió.",
|
||||
"skip_for_now": "Ignora-ho per ara",
|
||||
"account_created": "Compte creat",
|
||||
"enable_user_signups": "Activa l'alta d'usuaris",
|
||||
"enable_user_signups_description": "Decideix com poden donar-se d'alta els usuaris per crear comptes nous a Pocket ID.",
|
||||
"user_signups_are_disabled": "L'alta d'usuaris està deshabilitada actualment",
|
||||
"create_signup_token": "Crea token d'alta",
|
||||
"view_active_signup_tokens": "Mostra tokens d'alta actius",
|
||||
"manage_signup_tokens": "Gestiona tokens d'alta",
|
||||
"view_and_manage_active_signup_tokens": "Mostra i gestiona tokens d'alta actius.",
|
||||
"signup_token_deleted_successfully": "Token d'alta eliminat correctament.",
|
||||
"expired": "Caducat",
|
||||
"used_up": "Utilitzat",
|
||||
"active": "Actiu",
|
||||
"usage": "Ús",
|
||||
"created": "Creat",
|
||||
"token": "Token",
|
||||
"loading": "Carregant",
|
||||
"delete_signup_token": "Elimina token d'alta",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Segur que vols eliminar aquest token d'alta? Aquesta acció no es pot desfer.",
|
||||
"signup_with_token": "Dona't d'alta amb token",
|
||||
"signup_with_token_description": "Els usuaris només poden donar-se d'alta amb un token d'alta vàlid creat per un administrador.",
|
||||
"signup_open": "Procés d'alta obert",
|
||||
"signup_open_description": "Qualsevol pot crear-se un compte nou sense restriccions.",
|
||||
"of": "de",
|
||||
"skip_passkey_setup": "Omet la configuració de clau d'accés",
|
||||
"skip_passkey_setup_description": "Es recomana configurar una clau d'accés; sense ella, podries quedar-te fora quan caduqui la sessió.",
|
||||
"my_apps": "Les meves aplicacions",
|
||||
"no_apps_available": "No hi ha aplicacions disponibles",
|
||||
"contact_your_administrator_for_app_access": "Contacta amb l'administrador per obtenir accés a les aplicacions.",
|
||||
"launch": "Ves-hi",
|
||||
"client_launch_url": "URL d'accés del client",
|
||||
"client_launch_url_description": "L'URL que s'obrirà quan un usuari accedeix-hi a l'aplicació des de la pàgina Les meves aplicacions.",
|
||||
"client_name_description": "El nom del client que es mostra a la interfície de Pocket ID.",
|
||||
"revoke_access": "Revoca l'accés",
|
||||
"revoke_access_description": "Revoca l'accés a <b>{clientName}</b>. <b>{clientName}</b> ja no podrà accedir a la informació del teu compte.",
|
||||
"revoke_access_successful": "S'ha revocat correctament l'accés a {clientName}.",
|
||||
"last_signed_in_ago": "Últim inici de sessió fa {time}",
|
||||
"invalid_client_id": "L'ID del client només pot contenir lletres, números, guions baixos i guions",
|
||||
"custom_client_id_description": "Estableix un ID de client personalitzat si la teva aplicació ho requereix. Si no, deixa-ho en blanc per generar-ne un aleatori.",
|
||||
"generated": "Generat",
|
||||
"administration": "Administració",
|
||||
"group_rdn_attribute_description": "L'atribut utilitzat en el nom distingit (DN) dels grups.",
|
||||
"display_name_attribute": "Atribut del nom a mostrar",
|
||||
"display_name": "Nom a mostrar",
|
||||
"configure_application_images": "Configura les imatges de l'aplicació",
|
||||
"ui_config_disabled_info_title": "Configuració de la interfície d'usuari deshabilitada",
|
||||
"ui_config_disabled_info_description": "La configuració de la interfície d'usuari està deshabilitada perquè la configuració de l'aplicació es gestiona mitjançant variables d'entorn. Algunes opcions poden no ser editables.",
|
||||
"logo_from_url_description": "Enganxa directament una URL d'imatge (svg, png, webp). Troba icones a <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "URL invàlida",
|
||||
"require_user_email": "Requerir adreça de correu",
|
||||
"require_user_email_description": "Requereix que els usuaris tinguin una adreça de correu. Si està deshabilitat, els usuaris sense correu no podran utilitzar funcions que ho requereixen.",
|
||||
"view": "Veure",
|
||||
"toggle_columns": "Mostra/Amaga columnes",
|
||||
"locale": "Localització",
|
||||
"ldap_id": "ID LDAP",
|
||||
"reauthentication": "Reautenticació",
|
||||
"clear_filters": "Neteja filtres",
|
||||
"default_profile_picture": "Imatge de perfil per defecte",
|
||||
"light": "Clar",
|
||||
"dark": "Fosc",
|
||||
"system": "Sistema",
|
||||
"signup_token_user_groups_description": "Assigna automàticament aquests grups als usuaris que es donin d'alta amb aquest token.",
|
||||
"allowed_oidc_clients": "Clients OIDC permesos",
|
||||
"allowed_oidc_clients_description": "Selecciona els clients OIDC als quals els membres d'aquest grup poden iniciar la sessió.",
|
||||
"unrestrict_oidc_client": "Elimina restriccions a {clientName}",
|
||||
"confirm_unrestrict_oidc_client_description": "Segur que vols eliminar les restriccions del client OIDC <b>{clientName}</b>? Això eliminarà totes les assignacions de grup per aquest client i qualsevol usuari podrà iniciar la sessió.",
|
||||
"allowed_oidc_clients_updated_successfully": "Clients OIDC permesos actualitzats correctament",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"restricted": "Restrigit",
|
||||
"scim_provisioning": "Provisionament SCIM",
|
||||
"scim_provisioning_description": "El provisionament SCIM permet provisionar i desaprovisionar automàticament usuaris i grups des del teu client OIDC. Més informació als <link href='https://pocket-id.org/docs/configuration/scim'>docs</link>.",
|
||||
"scim_endpoint": "Endpoint SCIM",
|
||||
"scim_token": "Token SCIM",
|
||||
"last_successful_sync_at": "Última sincronització correcta: {time}",
|
||||
"scim_configuration_updated_successfully": "Configuració SCIM actualitzada correctament.",
|
||||
"scim_enabled_successfully": "SCIM habilitat correctament.",
|
||||
"scim_disabled_successfully": "SCIM deshabilitat correctament.",
|
||||
"disable_scim_provisioning": "Desactiva proveïment SCIM",
|
||||
"disable_scim_provisioning_confirm_description": "Segur que vols desactivar el proveïment SCIM per a <b>{clientName}</b>? Això aturarà la provisió automàtica d'usuaris i grups.",
|
||||
"scim_sync_failed": "La sincronització SCIM ha fallat. Consulta els registres del servidor per més informació.",
|
||||
"scim_sync_successful": "La sincronització SCIM s'ha completat correctament.",
|
||||
"save_and_sync": "Desa i sincronitza",
|
||||
"scim_save_changes_description": "Has de desar els canvis abans d'iniciar una sincronització SCIM. Vols desar-los ara?",
|
||||
"scopes": "Àmbits",
|
||||
"issuer_url": "URL de l'emissor",
|
||||
"smtp_field_required_when_other_provided": "Requerit quan s'ha proporcionat qualsevol configuració SMTP",
|
||||
"smtp_field_required_when_email_enabled": "Requerit quan les notificacions per correu estan habilitades",
|
||||
"renew": "Renova-ho",
|
||||
"renew_api_key": "Renova clau API",
|
||||
"renew_api_key_description": "Renovar la clau API generarà una nova clau. Assegura't d'actualitzar qualsevol integració que l'utilitzi.",
|
||||
"api_key_renewed": "Clau API renovada",
|
||||
"app_config_home_page": "Pàgina d'inici",
|
||||
"app_config_home_page_description": "La pàgina a la qual es redirigeix l'usuari després d'iniciar la sessió.",
|
||||
"email_verification_warning": "Verifica la teva adreça de correu",
|
||||
"email_verification_warning_description": "La teva adreça de correu encara no està verificada. Verifica-la tan aviat com sigui possible.",
|
||||
"email_verification": "Verificació de correu",
|
||||
"email_verification_description": "Envia un correu de verificació als usuaris quan es donguin d'alta o canvien la seva adreça de correu.",
|
||||
"email_verification_success_title": "Correu verificat correctament",
|
||||
"email_verification_success_description": "La teva adreça de correu ha estat verificada correctament.",
|
||||
"email_verification_error_title": "Error en la verificació de correu",
|
||||
"mark_as_unverified": "Marca com no verificat",
|
||||
"mark_as_verified": "Marca com verificat",
|
||||
"email_verification_sent": "Correu de verificació enviat correctament.",
|
||||
"emails_verified_by_default": "Correus verificats per defecte",
|
||||
"emails_verified_by_default_description": "Quan està habilitat, les adreces de correu dels usuaris seran marcades com verificades per defecte durant l'alta o en canviar la seva adreça.",
|
||||
"user_has_no_passkeys_yet": "Aquest usuari encara no té claus d'accés."
|
||||
}
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP adresa",
|
||||
"device": "Zařízení",
|
||||
"client": "Klient",
|
||||
"actor": "Herec",
|
||||
"unknown": "Neznámé",
|
||||
"account_details_updated_successfully": "Účet byl úspěšně aktualizován",
|
||||
"profile_picture_updated_successfully": "Profilový obrázek byl úspěšně aktualizován. Aktualizace může trvat několik minut.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Podrobnosti účtu",
|
||||
"passkeys": "Přístupové klíče",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Spravujte své přístupové klíče, které můžete použít pro ověření.",
|
||||
"manage_this_users_passkeys": "Přehled a správa přístupových klíčů tohoto uživatele.",
|
||||
"add_passkey": "Přidat přístupový klíč",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Vytvořte jednorázový přihlašovací kód pro přihlášení z jiného zařízení bez přístupového klíče.",
|
||||
"create": "Vytvořit",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Označit jako ověřené",
|
||||
"email_verification_sent": "Ověřovací e-mail byl úspěšně odeslán.",
|
||||
"emails_verified_by_default": "E-maily ověřené ve výchozím nastavení",
|
||||
"emails_verified_by_default_description": "Pokud je tato funkce povolena, budou e-mailové adresy uživatelů při registraci nebo při změně e-mailové adresy automaticky označeny jako ověřené."
|
||||
"emails_verified_by_default_description": "Pokud je tato funkce povolena, budou e-mailové adresy uživatelů při registraci nebo při změně e-mailové adresy automaticky označeny jako ověřené.",
|
||||
"user_has_no_passkeys_yet": "Tento uživatel zatím nemá žádné přístupové klíče."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-adresse",
|
||||
"device": "Enhed",
|
||||
"client": "Klient",
|
||||
"actor": "Skuespiller",
|
||||
"unknown": "Ukendt",
|
||||
"account_details_updated_successfully": "Kontodetaljer blev opdateret",
|
||||
"profile_picture_updated_successfully": "Profilbillede opdateret. Det kan tage et par minutter før ændringen vises.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Kontooplysninger",
|
||||
"passkeys": "Adgangsnøgler",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Administrér dine adgangsnøgler, som du kan bruge til at godkende dig selv.",
|
||||
"manage_this_users_passkeys": "Administrer denne brugers adgangskoder.",
|
||||
"add_passkey": "Tilføj adgangsnøgle",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Opret en engangskode for at logge ind fra en anden enhed uden en adgangsnøgle.",
|
||||
"create": "Opret",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Marker som verificeret",
|
||||
"email_verification_sent": "Bekræftelses-e-mail sendt med succes.",
|
||||
"emails_verified_by_default": "E-mails verificeret som standard",
|
||||
"emails_verified_by_default_description": "Når denne funktion er aktiveret, vil brugernes e-mailadresser som standard blive markeret som verificerede ved tilmelding eller når deres e-mailadresse ændres."
|
||||
"emails_verified_by_default_description": "Når denne funktion er aktiveret, vil brugernes e-mailadresser som standard blive markeret som verificerede ved tilmelding eller når deres e-mailadresse ændres.",
|
||||
"user_has_no_passkeys_yet": "Denne bruger har endnu ingen adgangskoder."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-Adresse",
|
||||
"device": "Gerät",
|
||||
"client": "Client",
|
||||
"actor": "Schauspieler",
|
||||
"unknown": "unbekannt",
|
||||
"account_details_updated_successfully": "Kontodetails erfolgreich aktualisiert",
|
||||
"profile_picture_updated_successfully": "Profilbild erfolgreich aktualisiert. Die Aktualisierung kann einige Minuten dauern.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Kontodetails",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Verwalte deine Passkeys, mit denen du dich authentifizieren kannst.",
|
||||
"manage_this_users_passkeys": "Verwalte die Passwörter dieses Benutzers.",
|
||||
"add_passkey": "Passkey hinzufügen",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Erzeuge einen einmaligen Anmeldecode, um dich ohne Passkey von einem anderen Gerät aus anzumelden.",
|
||||
"create": "Erzeugen",
|
||||
@@ -500,7 +502,7 @@
|
||||
"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",
|
||||
"scopes": "Scopes",
|
||||
"issuer_url": "Aussteller-URL",
|
||||
"smtp_field_required_when_other_provided": "Muss angegeben werden, wenn SMTP-Einstellungen gemacht werden",
|
||||
"smtp_field_required_when_email_enabled": "Muss aktiviert sein, wenn du E-Mail-Benachrichtigungen nutzen willst.",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Als verifiziert markieren",
|
||||
"email_verification_sent": "Bestätigungs-E-Mail erfolgreich verschickt.",
|
||||
"emails_verified_by_default": "E-Mails sind standardmäßig verifiziert",
|
||||
"emails_verified_by_default_description": "Wenn diese Option aktiviert ist, werden die E-Mail-Adressen der Nutzer bei der Anmeldung oder bei einer Änderung ihrer E-Mail-Adresse standardmäßig als verifiziert markiert."
|
||||
"emails_verified_by_default_description": "Wenn diese Option aktiviert ist, werden die E-Mail-Adressen der Nutzer bei der Anmeldung oder bei einer Änderung ihrer E-Mail-Adresse standardmäßig als verifiziert markiert.",
|
||||
"user_has_no_passkeys_yet": "Dieser Benutzer hat noch keine Passwörter."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"actor": "Actor",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Account Details",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"manage_this_users_passkeys": "Manage this user's passkeys.",
|
||||
"add_passkey": "Add Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Create",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Mark as verified",
|
||||
"email_verification_sent": "Verification email sent successfully.",
|
||||
"emails_verified_by_default": "Emails verified by default",
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed."
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed.",
|
||||
"user_has_no_passkeys_yet": "This user has no passkeys yet."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "Dirección IP",
|
||||
"device": "Dispositivo",
|
||||
"client": "Cliente",
|
||||
"actor": "Actor",
|
||||
"unknown": "Desconocido",
|
||||
"account_details_updated_successfully": "Detalles de la cuenta actualizados exitosamente",
|
||||
"profile_picture_updated_successfully": "Imagen de perfil actualizada correctamente. Puede tardar unos minutos en actualizarse.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Detalles de la cuenta",
|
||||
"passkeys": "Claves de acceso",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Administra las claves de acceso que te permiten autenticarte.",
|
||||
"manage_this_users_passkeys": "Gestiona las claves de acceso de este usuario.",
|
||||
"add_passkey": "Añade una clave de acceso",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Crea un código de inicio de sesión único para iniciar sesión desde un dispositivo diferente sin una clave.",
|
||||
"create": "Crear",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Marcar como verificado",
|
||||
"email_verification_sent": "El correo electrónico de verificación se ha enviado correctamente.",
|
||||
"emails_verified_by_default": "Correos electrónicos verificados de forma predeterminada",
|
||||
"emails_verified_by_default_description": "Cuando esta opción está activada, las direcciones de correo electrónico de los usuarios se marcarán como verificadas de forma predeterminada al registrarse o cuando se modifique su dirección de correo electrónico."
|
||||
"emails_verified_by_default_description": "Cuando esta opción está activada, las direcciones de correo electrónico de los usuarios se marcarán como verificadas de forma predeterminada al registrarse o cuando se modifique su dirección de correo electrónico.",
|
||||
"user_has_no_passkeys_yet": "Este usuario aún no tiene claves de acceso."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"actor": "Näitleja",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Account Details",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"manage_this_users_passkeys": "Haldage selle kasutaja paroolivõtmeid.",
|
||||
"add_passkey": "Add Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Create",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Mark as verified",
|
||||
"email_verification_sent": "Verification email sent successfully.",
|
||||
"emails_verified_by_default": "Emails verified by default",
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed."
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed.",
|
||||
"user_has_no_passkeys_yet": "Sellel kasutajal pole veel paroolivõtmeid."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-osoite",
|
||||
"device": "Laite",
|
||||
"client": "Asiakas",
|
||||
"actor": "Näyttelijä",
|
||||
"unknown": "Tuntematon",
|
||||
"account_details_updated_successfully": "Tilin tiedot päivitetty onnistuneesti",
|
||||
"profile_picture_updated_successfully": "Profiilikuva päivitetty onnistuneesti. Päivitys voi kestää muutaman minuutin.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Tilitiedot",
|
||||
"passkeys": "Pääsyavaimet",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Hallitse pääsyavaimiasi, joita voit käyttää tunnistautumiseen.",
|
||||
"manage_this_users_passkeys": "Hallitse tämän käyttäjän salasanat.",
|
||||
"add_passkey": "Lisää pääsyavain",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Luo kertakäyttöinen kirjautumiskoodi, jotta voit kirjautua sisään toisella laitteella ilman pääsyavainta.",
|
||||
"create": "Luo",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Merkitse vahvistetuksi",
|
||||
"email_verification_sent": "Vahvistussähköposti lähetetty onnistuneesti.",
|
||||
"emails_verified_by_default": "Sähköpostit vahvistettu oletuksena",
|
||||
"emails_verified_by_default_description": "Kun tämä toiminto on käytössä, käyttäjien sähköpostiosoitteet merkitään oletusarvoisesti vahvistetuiksi rekisteröitymisen yhteydessä tai kun heidän sähköpostiosoitteensa muuttuu."
|
||||
"emails_verified_by_default_description": "Kun tämä toiminto on käytössä, käyttäjien sähköpostiosoitteet merkitään oletusarvoisesti vahvistetuiksi rekisteröitymisen yhteydessä tai kun heidän sähköpostiosoitteensa muuttuu.",
|
||||
"user_has_no_passkeys_yet": "Tällä käyttäjällä ei ole vielä salasanakoodeja."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "Adresse IP",
|
||||
"device": "Périphérique",
|
||||
"client": "Application",
|
||||
"actor": "Acteur",
|
||||
"unknown": "Indisponible",
|
||||
"account_details_updated_successfully": "Les informations du compte ont été mises à jour avec succès",
|
||||
"profile_picture_updated_successfully": "La photo de profil a été mise à jour avec succès. La mise à jour peut prendre quelques minutes.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Paramètres du compte",
|
||||
"passkeys": "Clés d'accès",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gérez vos clés d'accès que vous pouvez utiliser pour vous authentifier.",
|
||||
"manage_this_users_passkeys": "Gérer les mots de passe de cet utilisateur.",
|
||||
"add_passkey": "Ajouter une clé d'accès",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Créez un code de connexion unique pour vous connecter depuis un autre appareil sans mot de passe.",
|
||||
"create": "Créer",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Marquer comme vérifié",
|
||||
"email_verification_sent": "L'e-mail de vérification a été envoyé sans problème.",
|
||||
"emails_verified_by_default": "E-mails vérifiés par défaut",
|
||||
"emails_verified_by_default_description": "Quand cette option est activée, les adresses e-mail des utilisateurs seront marquées comme vérifiées par défaut lors de leur inscription ou quand ils changent d'adresse e-mail."
|
||||
"emails_verified_by_default_description": "Quand cette option est activée, les adresses e-mail des utilisateurs seront marquées comme vérifiées par défaut lors de leur inscription ou quand ils changent d'adresse e-mail.",
|
||||
"user_has_no_passkeys_yet": "Cet utilisateur n'a pas encore de mots de passe."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "Indirizzo IP",
|
||||
"device": "Dispositivo",
|
||||
"client": "Client",
|
||||
"actor": "Autore",
|
||||
"unknown": "Sconosciuto",
|
||||
"account_details_updated_successfully": "Dettagli dell'account aggiornati con successo",
|
||||
"profile_picture_updated_successfully": "Immagine del profilo aggiornata con successo. Potrebbero essere necessari alcuni minuti per l'aggiornamento.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Dettagli account",
|
||||
"passkeys": "Passkey",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gestisci le tue passkey che puoi utilizzare per autenticarti.",
|
||||
"manage_this_users_passkeys": "Gestisci le chiavi di accesso di questo utente.",
|
||||
"add_passkey": "Aggiungi Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Crea un codice di accesso monouso per accedere da un dispositivo diverso senza una passkey.",
|
||||
"create": "Crea",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Contrassegna come verificato",
|
||||
"email_verification_sent": "Email di conferma inviata con successo.",
|
||||
"emails_verified_by_default": "Email verificate di default",
|
||||
"emails_verified_by_default_description": "Quando questa opzione è attiva, gli indirizzi email degli utenti saranno automaticamente contrassegnati come verificati al momento della registrazione o quando cambiano il loro indirizzo email."
|
||||
"emails_verified_by_default_description": "Quando questa opzione è attiva, gli indirizzi email degli utenti saranno automaticamente contrassegnati come verificati al momento della registrazione o quando cambiano il loro indirizzo email.",
|
||||
"user_has_no_passkeys_yet": "Questo utente non ha ancora nessuna chiave di accesso."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IPアドレス",
|
||||
"device": "デバイス",
|
||||
"client": "クライアント",
|
||||
"actor": "俳優",
|
||||
"unknown": "不明",
|
||||
"account_details_updated_successfully": "アカウントの詳細が正常に更新されました",
|
||||
"profile_picture_updated_successfully": "プロフィール画像が正常に更新されました。反映まで数分かかる場合があります。",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "アカウントの詳細",
|
||||
"passkeys": "パスキー",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "認証に使用するパスキーを管理します。",
|
||||
"manage_this_users_passkeys": "このユーザーのパスキーを管理します。",
|
||||
"add_passkey": "パスキーを追加",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "パスキーなしで別のデバイスからサインインするためのワンタイムログインコードを作成します。",
|
||||
"create": "作成",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "確認済みとしてマークする",
|
||||
"email_verification_sent": "確認メールが正常に送信されました。",
|
||||
"emails_verified_by_default": "メールはデフォルトで検証済み",
|
||||
"emails_verified_by_default_description": "有効化すると、ユーザーが登録時またはメールアドレスを変更した際に、デフォルトでメールアドレスが確認済みとしてマークされます。"
|
||||
"emails_verified_by_default_description": "有効化すると、ユーザーが登録時またはメールアドレスを変更した際に、デフォルトでメールアドレスが確認済みとしてマークされます。",
|
||||
"user_has_no_passkeys_yet": "このユーザーにはまだパスキーがありません。"
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP 주소",
|
||||
"device": "기기",
|
||||
"client": "클라이언트",
|
||||
"actor": "배우",
|
||||
"unknown": "알 수 없음",
|
||||
"account_details_updated_successfully": "계정 정보가 성공적으로 변경되었습니다",
|
||||
"profile_picture_updated_successfully": "프로필 사진이 성공적으로 변경되었습니다. 변경 적용까지 몇 분 정도 걸릴 수 있습니다.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "계정 세부 사항",
|
||||
"passkeys": "패스키",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "사용자 인증에 사용하는 패스키를 관리하세요.",
|
||||
"manage_this_users_passkeys": "이 사용자의 패스키를 관리합니다.",
|
||||
"add_passkey": "패스키 추가",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "패스키 없이 다른 기기에서 로그인하기 위한 일회용 로그인 코드를 생성합니다.",
|
||||
"create": "생성",
|
||||
@@ -490,7 +492,7 @@
|
||||
"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}",
|
||||
"last_successful_sync_at": "마지막 동기화 성공: {time}",
|
||||
"scim_configuration_updated_successfully": "SCIM 구성이 성공적으로 업데이트되었습니다.",
|
||||
"scim_enabled_successfully": "SCIM이 성공적으로 활성화되었습니다.",
|
||||
"scim_disabled_successfully": "SCIM이 성공적으로 비활성화되었습니다.",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "검증됨으로 표시",
|
||||
"email_verification_sent": "확인 이메일이 성공적으로 발송되었습니다.",
|
||||
"emails_verified_by_default": "이메일은 기본적으로 확인됨",
|
||||
"emails_verified_by_default_description": "이 기능이 활성화되면, 사용자의 이메일 주소는 가입 시 또는 이메일 주소 변경 시 기본적으로 확인된 상태로 표시됩니다."
|
||||
"emails_verified_by_default_description": "이 기능이 활성화되면, 사용자의 이메일 주소는 가입 시 또는 이메일 주소 변경 시 기본적으로 확인된 상태로 표시됩니다.",
|
||||
"user_has_no_passkeys_yet": "이 사용자는 아직 패스키가 없습니다."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-adres",
|
||||
"device": "Apparaat",
|
||||
"client": "Client",
|
||||
"actor": "Acteur",
|
||||
"unknown": "Onbekend",
|
||||
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
|
||||
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Accountgegevens",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de passkeys waarmee je jezelf kunt verifiëren.",
|
||||
"manage_this_users_passkeys": "Beheer de toegangscodes van deze gebruiker.",
|
||||
"add_passkey": "Passkey toevoegen",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Maak een eenmalige inlogcode aan om in te loggen vanaf een ander apparaat zonder passkey.",
|
||||
"create": "Aanmaken",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Markeer als geverifieerd",
|
||||
"email_verification_sent": "Verificatiemail is goed verstuurd.",
|
||||
"emails_verified_by_default": "E-mails standaard geverifieerd",
|
||||
"emails_verified_by_default_description": "Als je dit aan zet, worden de e-mailadressen van gebruikers standaard gemarkeerd als geverifieerd bij het aanmelden of als hun e-mailadres verandert."
|
||||
"emails_verified_by_default_description": "Als je dit aan zet, worden de e-mailadressen van gebruikers standaard gemarkeerd als geverifieerd bij het aanmelden of als hun e-mailadres verandert.",
|
||||
"user_has_no_passkeys_yet": "Deze gebruiker heeft nog geen toegangscodes."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP adresse",
|
||||
"device": "Enhet",
|
||||
"client": "Klient",
|
||||
"actor": "Actor",
|
||||
"unknown": "Ukjent",
|
||||
"account_details_updated_successfully": "Brukerdetaljer oppdatert",
|
||||
"profile_picture_updated_successfully": "Profilbildet er oppdatert. Det kan ta noen minutter før det vises overalt.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Kontodetaljer",
|
||||
"passkeys": "Passnøkler",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"manage_this_users_passkeys": "Manage this user's passkeys.",
|
||||
"add_passkey": "Legg til passnøkkel",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Opprett",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Mark as verified",
|
||||
"email_verification_sent": "Verification email sent successfully.",
|
||||
"emails_verified_by_default": "Emails verified by default",
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed."
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed.",
|
||||
"user_has_no_passkeys_yet": "This user has no passkeys yet."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "Adres IP",
|
||||
"device": "Urządzenie",
|
||||
"client": "Klient",
|
||||
"actor": "Aktor",
|
||||
"unknown": "Nieznany",
|
||||
"account_details_updated_successfully": "Sukces! Szczegóły konta zostały zaktualizowane.",
|
||||
"profile_picture_updated_successfully": "Sukces! Zdjęcie profilowe zostało zaktualizowane. Może to potrwać kilka minut.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Szczegóły konta",
|
||||
"passkeys": "Klucze",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Zarządzaj swoimi kluczami, których możesz użyć do uwierzytelnienia siebie.",
|
||||
"manage_this_users_passkeys": "Zarządzaj kluczami dostępu tego użytkownika.",
|
||||
"add_passkey": "Dodaj klucz",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Utwórz jednorazowy kod logowania, aby zalogować się z innego urządzenia bez klucza.",
|
||||
"create": "Utwórz",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Oznacz jako zweryfikowane",
|
||||
"email_verification_sent": "Wiadomość e-mail z linkiem weryfikacyjnym została wysłana.",
|
||||
"emails_verified_by_default": "E-maile weryfikowane domyślnie",
|
||||
"emails_verified_by_default_description": "Po włączeniu tej opcji adresy e-mail użytkowników będą domyślnie oznaczane jako zweryfikowane podczas rejestracji lub zmiany adresu e-mail."
|
||||
"emails_verified_by_default_description": "Po włączeniu tej opcji adresy e-mail użytkowników będą domyślnie oznaczane jako zweryfikowane podczas rejestracji lub zmiany adresu e-mail.",
|
||||
"user_has_no_passkeys_yet": "Ten użytkownik nie ma jeszcze żadnych kluczy dostępu."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "Endereço de IP",
|
||||
"device": "Dispositivo",
|
||||
"client": "Cliente",
|
||||
"actor": "Ator",
|
||||
"unknown": "Desconhecido",
|
||||
"account_details_updated_successfully": "Detalhes da conta atualizados com sucesso",
|
||||
"profile_picture_updated_successfully": "Foto de perfil alterada com sucesso. A atualização pode demorar alguns minutos.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Detalhes da Conta",
|
||||
"passkeys": "Chaves de acesso",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gerencie as chaves de acesso que pode usar para se identificar.",
|
||||
"manage_this_users_passkeys": "Gerencie as senhas deste usuário.",
|
||||
"add_passkey": "Adicionar chave de acesso",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Crie um código de login único para entrar em outro dispositivo sem precisar da chave de acesso.",
|
||||
"create": "Criar",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Marcar como verificado",
|
||||
"email_verification_sent": "E-mail de verificação enviado com sucesso.",
|
||||
"emails_verified_by_default": "E-mails verificados por padrão",
|
||||
"emails_verified_by_default_description": "Quando ativado, os endereços de e-mail dos usuários serão marcados como verificados por padrão no momento da inscrição ou quando o endereço de e-mail for alterado."
|
||||
"emails_verified_by_default_description": "Quando ativado, os endereços de e-mail dos usuários serão marcados como verificados por padrão no momento da inscrição ou quando o endereço de e-mail for alterado.",
|
||||
"user_has_no_passkeys_yet": "Esse usuário ainda não tem senhas."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"actor": "Ator",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Account Details",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"manage_this_users_passkeys": "Gerir as chaves de acesso deste utilizador.",
|
||||
"add_passkey": "Add Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Create",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Mark as verified",
|
||||
"email_verification_sent": "Verification email sent successfully.",
|
||||
"emails_verified_by_default": "Emails verified by default",
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed."
|
||||
"emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed.",
|
||||
"user_has_no_passkeys_yet": "Este utilizador ainda não tem chaves de acesso."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-адрес",
|
||||
"device": "Устройство",
|
||||
"client": "Клиент",
|
||||
"actor": "Актёр",
|
||||
"unknown": "Неизвестно",
|
||||
"account_details_updated_successfully": "Данные учетной записи успешно обновлены",
|
||||
"profile_picture_updated_successfully": "Изображение профиля успешно обновлено. Обновление может занять несколько минут.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Детали учетной записи",
|
||||
"passkeys": "Ключи доступа",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте своими ключами доступа, которые вы можете использовать для аутентификации.",
|
||||
"manage_this_users_passkeys": "Управлять паролями этого пользователя.",
|
||||
"add_passkey": "Добавить ключ доступа",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без ключа доступа.",
|
||||
"create": "Создать",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Пометить как подтвержденную",
|
||||
"email_verification_sent": "Письмо с подтверждением успешно отправлено.",
|
||||
"emails_verified_by_default": "Электронные почты подтверждены по умолчанию",
|
||||
"emails_verified_by_default_description": "Если эта функция включена, адреса электронной почты пользователей будут по умолчанию помечаться как подтверждённые при регистрации или при смене адреса."
|
||||
"emails_verified_by_default_description": "Если эта функция включена, адреса электронной почты пользователей будут по умолчанию помечаться как подтверждённые при регистрации или при смене адреса.",
|
||||
"user_has_no_passkeys_yet": "У этого пользователя пока нет паролей."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-adress",
|
||||
"device": "Enhet",
|
||||
"client": "Klient",
|
||||
"actor": "Skådespelare",
|
||||
"unknown": "Okänd",
|
||||
"account_details_updated_successfully": "Kontouppgifterna har uppdaterats",
|
||||
"profile_picture_updated_successfully": "Profilbilden har uppdaterats. Det kan ta några minuter innan uppdateringen syns.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Kontouppgifter",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Hantera dina passkeys som du kan använda för att autentisera dig själv.",
|
||||
"manage_this_users_passkeys": "Hantera den här användarens lösenord.",
|
||||
"add_passkey": "Lägg till en passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Skapa en engångskod för att logga in från en annan enhet utan en passkey.",
|
||||
"create": "Skapa",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Markera som verifierad",
|
||||
"email_verification_sent": "Verifieringsmeddelandet har skickats.",
|
||||
"emails_verified_by_default": "E-postmeddelanden verifierade som standard",
|
||||
"emails_verified_by_default_description": "När funktionen är aktiverad kommer användarnas e-postadresser att markeras som verifierade som standard vid registrering eller när deras e-postadress ändras."
|
||||
"emails_verified_by_default_description": "När funktionen är aktiverad kommer användarnas e-postadresser att markeras som verifierade som standard vid registrering eller när deras e-postadress ändras.",
|
||||
"user_has_no_passkeys_yet": "Den här användaren har inga lösenord ännu."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP Adresi",
|
||||
"device": "Cihaz",
|
||||
"client": "İstemci",
|
||||
"actor": "Oyuncu",
|
||||
"unknown": "Bilinmeyen",
|
||||
"account_details_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
|
||||
"profile_picture_updated_successfully": "Profil resmi başarıyla güncellendi. Güncellenmesi birkaç dakika sürebilir.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Hesap Bilgileri",
|
||||
"passkeys": "Geçiş Anahtarları",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Kendinizi doğrulamak için kullanabileceğiniz geçiş anahtarlarınızı yönetin.",
|
||||
"manage_this_users_passkeys": "Bu kullanıcının şifre anahtarlarını yönet.",
|
||||
"add_passkey": "Geçiş Anahtarı Ekle",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Geçiş anahtarı olmadan farklı bir cihazdan giriş yapmak için tek kullanımlık bir giriş kodu oluşturun.",
|
||||
"create": "Oluştur",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Doğrulanmış olarak işaretle",
|
||||
"email_verification_sent": "Doğrulama e-postası başarıyla gönderildi.",
|
||||
"emails_verified_by_default": "Varsayılan olarak doğrulanmış e-postalar",
|
||||
"emails_verified_by_default_description": "Etkinleştirildiğinde, kullanıcıların e-posta adresleri kayıt sırasında veya e-posta adresleri değiştirildiğinde varsayılan olarak doğrulanmış olarak işaretlenecektir."
|
||||
"emails_verified_by_default_description": "Etkinleştirildiğinde, kullanıcıların e-posta adresleri kayıt sırasında veya e-posta adresleri değiştirildiğinde varsayılan olarak doğrulanmış olarak işaretlenecektir.",
|
||||
"user_has_no_passkeys_yet": "Bu kullanıcının henüz şifre anahtarı yok."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP-адреса",
|
||||
"device": "Пристрій",
|
||||
"client": "Клієнт",
|
||||
"actor": "Актор",
|
||||
"unknown": "Невідомий",
|
||||
"account_details_updated_successfully": "Дані облікового запису успішно оновлені",
|
||||
"profile_picture_updated_successfully": "Зображення профілю успішно оновлено. Оновлення може зайняти кілька хвилин.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Дані облікового запису",
|
||||
"passkeys": "Ключі доступу",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Керуйте своїми ключами доступу, які ви можете використовувати для автентифікації.",
|
||||
"manage_this_users_passkeys": "Керувати паролями цього користувача.",
|
||||
"add_passkey": "Додати ключ доступу",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Створіть одноразовий код для входу, щоб увійти з іншого пристрою без ключа доступу.",
|
||||
"create": "Створити",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Позначити як підтверджену",
|
||||
"email_verification_sent": "Електронний лист для підтвердження успішно надіслано.",
|
||||
"emails_verified_by_default": "Електронні адреси підтверджені за замовчуванням",
|
||||
"emails_verified_by_default_description": "Якщо увімкнено, електронні адреси користувачів будуть автоматично позначатися як підтверджені під час реєстрації або зміни електронної адреси."
|
||||
"emails_verified_by_default_description": "Якщо увімкнено, електронні адреси користувачів будуть автоматично позначатися як підтверджені під час реєстрації або зміни електронної адреси.",
|
||||
"user_has_no_passkeys_yet": "Цей користувач ще не має паролів."
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Xóa yêu cầu tùy chỉnh",
|
||||
"add_custom_claim": "Thêm yêu cầu tùy chỉnh",
|
||||
"add_another": "Thêm khác",
|
||||
"add_another": "Thêm một cái nữa",
|
||||
"select_a_date": "Chọn ngày",
|
||||
"select_file": "Chọn tập tin",
|
||||
"select_file": "Chọn file",
|
||||
"profile_picture": "Ảnh đại diện",
|
||||
"profile_picture_is_managed_by_ldap_server": "Hình đại diện được quản lý bởi máy chủ LDAP và không thể thay đổi tại đây.",
|
||||
"click_profile_picture_to_upload_custom": "Nhấp vào hình ảnh hồ sơ để tải lên hình ảnh tùy chỉnh.",
|
||||
@@ -22,15 +22,15 @@
|
||||
"expand_card": "Mở rộng thẻ",
|
||||
"copied": "Đã sao chép",
|
||||
"click_to_copy": "Nhấn để sao chép",
|
||||
"something_went_wrong": "Đã xảy ra lỗi",
|
||||
"something_went_wrong": "Không ổn rồi",
|
||||
"go_back_to_home": "Quay lại trang chủ",
|
||||
"alternative_sign_in_methods": "Các phương thức đăng nhập thay thế",
|
||||
"login_background": "Hình nền đăng nhập",
|
||||
"alternative_sign_in_methods": "Phương thức đăng nhập thay thế",
|
||||
"login_background": "Hình nền màn hình đăng nhập",
|
||||
"logo": "Logo",
|
||||
"login_code": "Mã đăng nhập",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Tạo một mã đăng nhập mà người dùng có thể sử dụng để đăng nhập mà không cần passkey (một lần).",
|
||||
"one_hour": "1 giờ",
|
||||
"twelve_hours": "12 giờ",
|
||||
"one_hour": "1 tiếng",
|
||||
"twelve_hours": "12 tiếng",
|
||||
"one_day": "1 ngày",
|
||||
"one_week": "1 tuần",
|
||||
"one_month": "1 tháng",
|
||||
@@ -39,13 +39,13 @@
|
||||
"name": "Tên",
|
||||
"browser_unsupported": "Trình duyệt không được hỗ trợ",
|
||||
"this_browser_does_not_support_passkeys": "Trình duyệt này không hỗ trợ passkeys. Vui lòng sử dụng phương thức đăng nhập khác.",
|
||||
"an_unknown_error_occurred": "Một lỗi không rõ đã xảy ra",
|
||||
"an_unknown_error_occurred": "Một lỗi chưa biết đã xảy ra",
|
||||
"authentication_process_was_aborted": "Quá trình xác thực đã bị hủy bỏ",
|
||||
"error_occurred_with_authenticator": "Đã xảy ra lỗi với trình xác thực",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Thiết bị xác thực không hỗ trợ discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "Thiết bị xác thực không hỗ trợ khóa lưu trữ",
|
||||
"passkey_was_previously_registered": "Passkey này đã được đăng ký trước đó",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Thiết bị xác thực không hỗ trợ bất kỳ thuật toán nào trong số các thuật toán được yêu cầu",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Thiết bị xác thực không hỗ trợ tìm kiếm chứng chỉ tự động",
|
||||
"authenticator_does_not_support_resident_keys": "Thiết bị xác thực không hỗ trợ khóa nhà",
|
||||
"passkey_was_previously_registered": "Passkey này đã được đăng ký trước đó rồi",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Thiết bị xác thực không hỗ trợ bất kỳ phương pháp nào trong số các phương pháp được yêu cầu",
|
||||
"webauthn_error_invalid_rp_id": "ID của bên tin cậy đã cấu hình là không hợp lệ.",
|
||||
"webauthn_error_invalid_domain": "Domain đã cấu hình không hợp lệ.",
|
||||
"contact_administrator_to_fix": "Liên hệ với quản trị viên của bạn để khắc phục sự cố này.",
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "Địa chỉ IP",
|
||||
"device": "Thiết bị",
|
||||
"client": "Client.",
|
||||
"actor": "Diễn viên",
|
||||
"unknown": "Không xác định",
|
||||
"account_details_updated_successfully": "Cập nhật tài khoản thành công",
|
||||
"profile_picture_updated_successfully": "Hình đại diện đã được cập nhật thành công. Việc cập nhật có thể mất vài phút.",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "Thông Tin Tài Khoản",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Quản lý các passkeys mà bạn có thể sử dụng để xác thực danh tính của mình.",
|
||||
"manage_this_users_passkeys": "Quản lý các khóa truy cập của người dùng này.",
|
||||
"add_passkey": "Thêm Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Tạo mã đăng nhập một lần để đăng nhập từ thiết bị khác mà không cần passkeys.",
|
||||
"create": "Tạo",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "Đánh dấu là đã xác minh",
|
||||
"email_verification_sent": "Email xác minh đã được gửi thành công.",
|
||||
"emails_verified_by_default": "Email được xác minh theo mặc định",
|
||||
"emails_verified_by_default_description": "Khi tính năng này được kích hoạt, địa chỉ email của người dùng sẽ được đánh dấu là đã xác minh theo mặc định khi đăng ký hoặc khi địa chỉ email của họ được thay đổi."
|
||||
"emails_verified_by_default_description": "Khi tính năng này được kích hoạt, địa chỉ email của người dùng sẽ được đánh dấu là đã xác minh theo mặc định khi đăng ký hoặc khi địa chỉ email của họ được thay đổi.",
|
||||
"user_has_no_passkeys_yet": "Người dùng này hiện chưa có khóa truy cập nào."
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP 地址",
|
||||
"device": "设备",
|
||||
"client": "客户端",
|
||||
"actor": "演员",
|
||||
"unknown": "未知",
|
||||
"account_details_updated_successfully": "账户信息已成功更新",
|
||||
"profile_picture_updated_successfully": "头像更新成功。可能需要几分钟才能完成刷新。",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "账户详情",
|
||||
"passkeys": "通行密钥",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "管理您的通行密钥,以用于身份验证。",
|
||||
"manage_this_users_passkeys": "管理此用户的密钥。",
|
||||
"add_passkey": "添加通行密钥",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "创建一次性登录码,以便从不同设备登录而无需通行密钥。",
|
||||
"create": "创建",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "标记为已验证",
|
||||
"email_verification_sent": "验证邮件已成功发送。",
|
||||
"emails_verified_by_default": "电子邮件默认已验证",
|
||||
"emails_verified_by_default_description": "启用后,用户的电子邮件地址将在注册时或更改电子邮件地址时默认标记为已验证。"
|
||||
"emails_verified_by_default_description": "启用后,用户的电子邮件地址将在注册时或更改电子邮件地址时默认标记为已验证。",
|
||||
"user_has_no_passkeys_yet": "该用户目前还没有密钥。"
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"ip_address": "IP 地址",
|
||||
"device": "裝置",
|
||||
"client": "客戶端",
|
||||
"actor": "演員",
|
||||
"unknown": "未知",
|
||||
"account_details_updated_successfully": "帳號資訊更新成功",
|
||||
"profile_picture_updated_successfully": "個人資料圖片更新成功。 這可能會花幾分鐘更新。",
|
||||
@@ -117,6 +118,7 @@
|
||||
"account_details": "帳號詳細資料",
|
||||
"passkeys": "密碼金鑰",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "管理可用於驗證身分的密碼金鑰。",
|
||||
"manage_this_users_passkeys": "管理此使用者的密鑰。",
|
||||
"add_passkey": "新增密碼金鑰",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "建立一次性登入代碼,以便從其他裝置登入而不需使用密碼金鑰。",
|
||||
"create": "建立",
|
||||
@@ -521,5 +523,6 @@
|
||||
"mark_as_verified": "標記為已驗證",
|
||||
"email_verification_sent": "驗證電子郵件已成功寄出。",
|
||||
"emails_verified_by_default": "電子郵件預設為已驗證",
|
||||
"emails_verified_by_default_description": "啟用此功能後,用戶的電子郵件地址將在註冊時或變更電子郵件地址時,預設標記為已驗證狀態。"
|
||||
"emails_verified_by_default_description": "啟用此功能後,用戶的電子郵件地址將在註冊時或變更電子郵件地址時,預設標記為已驗證狀態。",
|
||||
"user_has_no_passkeys_yet": "此使用者目前尚未擁有任何密鑰。"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "2.4.0",
|
||||
"version": "2.6.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -14,51 +14,51 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"axios": "^1.13.5",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"axios": "^1.15.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"jose": "^6.1.3",
|
||||
"jose": "^6.2.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"runed": "^0.37.1",
|
||||
"sveltekit-superforms": "^2.30.0",
|
||||
"sveltekit-superforms": "^2.30.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.12.0",
|
||||
"@inlang/plugin-m-function-matcher": "^2.2.1",
|
||||
"@inlang/plugin-message-format": "^4.3.0",
|
||||
"@internationalized/date": "^3.11.0",
|
||||
"@inlang/paraglide-js": "^2.15.3",
|
||||
"@inlang/plugin-m-function-matcher": "^2.2.4",
|
||||
"@inlang/plugin-message-format": "^4.4.0",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^0.559.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/kit": "^2.57.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "^24.10.13",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"bits-ui": "^2.16.2",
|
||||
"eslint": "^9.39.3",
|
||||
"bits-ui": "^2.17.3",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"formsnap": "^2.0.1",
|
||||
"globals": "^16.5.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.0",
|
||||
"prettier": "^3.8.2",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"rollup": "^4.59.0",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-check": "^4.4.3",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
"rollup": "^4.60.1",
|
||||
"svelte": "^5.55.3",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tslib": "^2.8.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"vite": "^7.3.1",
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-compression": "^0.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"baseLocale": "en",
|
||||
"locales": [
|
||||
"ca",
|
||||
"cs",
|
||||
"da",
|
||||
"de",
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
hidden: !isAdmin,
|
||||
value: (item) => item.username ?? m.unknown()
|
||||
},
|
||||
{
|
||||
label: m.actor(),
|
||||
key: 'actorUsername',
|
||||
hidden: !isAdmin,
|
||||
value: (item) => item.actorUsername ?? m.unknown()
|
||||
},
|
||||
{
|
||||
label: m.event(),
|
||||
column: 'event',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import FormattedMessage from '../formatted-message.svelte';
|
||||
import { cn } from '$lib/utils/style';
|
||||
|
||||
type WithoutChildren = {
|
||||
children?: undefined;
|
||||
@@ -48,44 +49,53 @@
|
||||
const id = label?.toLowerCase().replace(/ /g, '-');
|
||||
</script>
|
||||
|
||||
<Field.Field data-disabled={disabled} {...restProps}>
|
||||
{#if label}
|
||||
<Field.Label required={input?.required} class="mb-0" for={labelFor ?? id}>{label}</Field.Label>
|
||||
{/if}
|
||||
{#if description}
|
||||
<Field.Description>
|
||||
<FormattedMessage m={description} />
|
||||
{#if docsLink}
|
||||
<a
|
||||
class="relative text-black after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:translate-y-[-1px] after:bg-white dark:text-white"
|
||||
href={docsLink}
|
||||
target="_blank"
|
||||
>
|
||||
{m.docs()}
|
||||
<LucideExternalLink class="inline size-3 align-text-top" />
|
||||
</a>
|
||||
{/if}
|
||||
</Field.Description>
|
||||
{/if}
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if input}
|
||||
{#if type === 'date'}
|
||||
<DatePicker {id} bind:value={input.value as Date} />
|
||||
{:else}
|
||||
<Input
|
||||
aria-invalid={!!input.error}
|
||||
class={inputClass}
|
||||
{id}
|
||||
{placeholder}
|
||||
{type}
|
||||
bind:value={input.value}
|
||||
{disabled}
|
||||
oninput={(e) => onInput?.(e)}
|
||||
/>
|
||||
<Field.Field
|
||||
data-disabled={disabled}
|
||||
class={cn('flex flex-col justify-between', restProps.class)}
|
||||
{...restProps}
|
||||
>
|
||||
<div>
|
||||
{#if label}
|
||||
<Field.Label required={input?.required} class="mb-0" for={labelFor ?? id}>{label}</Field.Label
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if input?.error}
|
||||
<Field.Error class="text-start">{input.error}</Field.Error>
|
||||
{/if}
|
||||
{#if description}
|
||||
<Field.Description>
|
||||
<FormattedMessage m={description} />
|
||||
{#if docsLink}
|
||||
<a
|
||||
class="relative text-black after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:translate-y-[-1px] after:bg-white dark:text-white"
|
||||
href={docsLink}
|
||||
target="_blank"
|
||||
>
|
||||
{m.docs()}
|
||||
<LucideExternalLink class="inline size-3 align-text-top" />
|
||||
</a>
|
||||
{/if}
|
||||
</Field.Description>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if input}
|
||||
{#if type === 'date'}
|
||||
<DatePicker {id} bind:value={input.value as Date} />
|
||||
{:else}
|
||||
<Input
|
||||
aria-invalid={!!input.error}
|
||||
class={inputClass}
|
||||
{id}
|
||||
{placeholder}
|
||||
{type}
|
||||
bind:value={input.value}
|
||||
{disabled}
|
||||
oninput={(e) => onInput?.(e)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if input?.error}
|
||||
<Field.Error class="text-start">{input.error}</Field.Error>
|
||||
{/if}
|
||||
</div>
|
||||
</Field.Field>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script module lang="ts">
|
||||
// Persist the last failing background image URL across route remounts so
|
||||
// login pages without a background do not briefly retry the image and stutter.
|
||||
let persistedMissingBackgroundImageUrl: string | undefined;
|
||||
let backgroundImageExists = $state<boolean | undefined>(undefined);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -11,8 +9,9 @@
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import { cachedBackgroundImage } from '$lib/utils/cached-image-util';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { type Snippet } from 'svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
import { fade } from 'svelte/transition';
|
||||
import * as Card from './ui/card';
|
||||
|
||||
let {
|
||||
@@ -23,33 +22,22 @@
|
||||
showAlternativeSignInMethodButton?: boolean;
|
||||
} = $props();
|
||||
|
||||
let missingBackgroundImageUrl = $state<string | undefined>(persistedMissingBackgroundImageUrl);
|
||||
let loadedBackgroundImageUrl = $state<string | undefined>();
|
||||
let isInitialLoad = $state(false);
|
||||
let backgroundImageUrl = $derived(cachedBackgroundImage.getUrl());
|
||||
let imageError = $derived(missingBackgroundImageUrl === backgroundImageUrl);
|
||||
let imageLoaded = $derived(loadedBackgroundImageUrl === backgroundImageUrl);
|
||||
let animate = $derived(isInitialLoad && imageLoaded && !$appConfigStore.disableAnimations);
|
||||
let animate = $derived(isInitialLoad && !$appConfigStore.disableAnimations);
|
||||
|
||||
onMount(async () => {
|
||||
fetch(cachedBackgroundImage.getUrl(), {
|
||||
method: 'HEAD'
|
||||
})
|
||||
.then(async (res) => (backgroundImageExists = res.ok))
|
||||
.catch(() => (backgroundImageExists = false));
|
||||
});
|
||||
|
||||
afterNavigate((e) => {
|
||||
isInitialLoad = !e?.from?.url;
|
||||
});
|
||||
|
||||
function onBackgroundImageLoad() {
|
||||
loadedBackgroundImageUrl = backgroundImageUrl;
|
||||
if (persistedMissingBackgroundImageUrl === backgroundImageUrl) {
|
||||
persistedMissingBackgroundImageUrl = undefined;
|
||||
missingBackgroundImageUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onBackgroundImageError() {
|
||||
loadedBackgroundImageUrl = undefined;
|
||||
persistedMissingBackgroundImageUrl = backgroundImageUrl;
|
||||
missingBackgroundImageUrl = backgroundImageUrl;
|
||||
}
|
||||
|
||||
const isDesktop = new MediaQuery('min-width: 1024px');
|
||||
const isDesktop = new MediaQuery('(min-width: 1024px)');
|
||||
let alternativeSignInButton = $state({
|
||||
href: '/login/alternative',
|
||||
label: m.alternative_sign_in_methods()
|
||||
@@ -70,11 +58,19 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isDesktop.current}
|
||||
<div class="h-screen items-center overflow-hidden text-center flex justify-center">
|
||||
{#if backgroundImageExists === undefined}
|
||||
<div class="bg-background h-screen"></div>
|
||||
{:else if isDesktop.current}
|
||||
<div
|
||||
in:fade={{ duration: 150 }}
|
||||
class="relative flex h-screen w-full items-center overflow-hidden text-center {backgroundImageExists
|
||||
? 'justify-start'
|
||||
: 'justify-center'}"
|
||||
>
|
||||
<div
|
||||
class="flex h-full w-[650px] 2xl:w-[800px] p-16 {cn(
|
||||
showAlternativeSignInMethodButton && 'pb-0'
|
||||
class="relative z-10 flex h-full p-16 {cn(
|
||||
showAlternativeSignInMethodButton && 'pb-0',
|
||||
backgroundImageExists && 'w-[650px] 2xl:w-[800px]'
|
||||
)}"
|
||||
>
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
@@ -94,15 +90,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !imageError}
|
||||
{#if backgroundImageExists}
|
||||
<!-- Background image -->
|
||||
<div class="m-6 flex h-[calc(100vh-3rem)] overflow-hidden rounded-[40px]">
|
||||
<div
|
||||
class="absolute top-0 right-0 bottom-0 left-[650px] z-0 m-6 overflow-hidden rounded-[40px] 2xl:left-[800px]"
|
||||
>
|
||||
<img
|
||||
src={backgroundImageUrl}
|
||||
class="h-full object-cover {cn(animate && 'animate-bg-zoom')}"
|
||||
src={cachedBackgroundImage.getUrl()}
|
||||
class="{cn(
|
||||
animate && 'animate-bg-zoom'
|
||||
)} h-screen object-cover w-[calc(100vw-650px)] 2xl:w-[calc(100vw-800px)]"
|
||||
alt={m.login_background()}
|
||||
onload={onBackgroundImageLoad}
|
||||
onerror={onBackgroundImageError}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -112,9 +110,14 @@
|
||||
class="flex h-screen items-center justify-center bg-cover bg-center text-center"
|
||||
style="background-image: url({cachedBackgroundImage.getUrl()});"
|
||||
>
|
||||
<Card.Root class="mx-3 w-full max-w-md">
|
||||
<Card.Root
|
||||
class={{
|
||||
'mx-3 w-full max-w-md': true,
|
||||
'bg-transparent border-0': !backgroundImageExists
|
||||
}}
|
||||
>
|
||||
<Card.CardContent
|
||||
class="px-4 py-10 sm:p-10 {showAlternativeSignInMethodButton ? 'pb-3 sm:pb-3' : ''}"
|
||||
class="px-4 py-10 sm:p-10 {showAlternativeSignInMethodButton ? 'pb-3 sm:pb-3' : ''} "
|
||||
>
|
||||
{@render children()}
|
||||
{#if showAlternativeSignInMethodButton}
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
icon,
|
||||
onRename,
|
||||
onDelete,
|
||||
showRenameAction = true,
|
||||
label,
|
||||
description
|
||||
}: {
|
||||
icon: typeof IconType;
|
||||
onRename: () => void;
|
||||
onRename?: () => void;
|
||||
onDelete: () => void;
|
||||
showRenameAction?: boolean;
|
||||
description?: string;
|
||||
label?: string;
|
||||
} = $props();
|
||||
@@ -36,22 +38,24 @@
|
||||
{/if}
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
onclick={onRename}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="size-8"
|
||||
aria-label={m.rename()}
|
||||
>
|
||||
<LucidePencil class="size-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{m.rename()}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
{#if showRenameAction && onRename}
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
onclick={onRename}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="size-8"
|
||||
aria-label={m.rename()}
|
||||
>
|
||||
<LucidePencil class="size-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{m.rename()}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
|
||||
@@ -22,7 +22,8 @@ class OidcService extends APIService {
|
||||
nonce?: string,
|
||||
codeChallenge?: string,
|
||||
codeChallengeMethod?: string,
|
||||
reauthenticationToken?: string
|
||||
reauthenticationToken?: string,
|
||||
prompt?: string
|
||||
) => {
|
||||
const res = await this.api.post('/oidc/authorize', {
|
||||
scope,
|
||||
@@ -31,7 +32,8 @@ class OidcService extends APIService {
|
||||
clientId,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
reauthenticationToken
|
||||
reauthenticationToken,
|
||||
prompt
|
||||
});
|
||||
|
||||
return res.data as AuthorizeResponse;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import type { SignupToken } from '$lib/types/signup-token.type';
|
||||
import type { UserGroup } from '$lib/types/user-group.type';
|
||||
import type { AccountUpdate, User, UserCreate, UserSignUp } from '$lib/types/user.type';
|
||||
@@ -33,6 +34,11 @@ export default class UserService extends APIService {
|
||||
return res.data as UserGroup[];
|
||||
};
|
||||
|
||||
listUserPasskeys = async (userId: string) => {
|
||||
const res = await this.api.get(`/users/${userId}/webauthn-credentials`);
|
||||
return res.data as Passkey[];
|
||||
};
|
||||
|
||||
update = async (id: string, user: UserCreate) => {
|
||||
const res = await this.api.put(`/users/${id}`, user);
|
||||
return res.data as User;
|
||||
@@ -47,6 +53,10 @@ export default class UserService extends APIService {
|
||||
await this.api.delete(`/users/${id}`);
|
||||
};
|
||||
|
||||
removeUserPasskey = async (userId: string, passkeyId: string) => {
|
||||
await this.api.delete(`/users/${userId}/webauthn-credentials/${passkeyId}`);
|
||||
};
|
||||
|
||||
updateProfilePicture = async (userId: string, image: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', image!);
|
||||
@@ -113,6 +123,10 @@ export default class UserService extends APIService {
|
||||
return res.data as User;
|
||||
};
|
||||
|
||||
checkInitialUserSetupAvailable = async () => {
|
||||
await this.api.get('/signup/setup');
|
||||
};
|
||||
|
||||
listSignupTokens = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/signup-tokens', { params: options });
|
||||
return res.data as Paginated<SignupToken>;
|
||||
|
||||
@@ -7,6 +7,7 @@ export type AuditLog = {
|
||||
device: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
actorUsername?: string;
|
||||
createdAt: string;
|
||||
data: any;
|
||||
};
|
||||
|
||||
@@ -62,9 +62,11 @@ export type OidcDeviceCodeInfo = {
|
||||
};
|
||||
|
||||
export type AuthorizeResponse = {
|
||||
code: string;
|
||||
callbackURL: string;
|
||||
issuer: string;
|
||||
code?: string;
|
||||
callbackURL?: string;
|
||||
issuer?: string;
|
||||
error?: string;
|
||||
requiresRedirect?: boolean;
|
||||
};
|
||||
|
||||
export type AccessibleOidcClient = OidcClientMetaData & {
|
||||
|
||||
@@ -20,8 +20,16 @@
|
||||
const oidService = new OidcService();
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let { client, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod, authorizeState } =
|
||||
data;
|
||||
let {
|
||||
client,
|
||||
scope,
|
||||
callbackURL,
|
||||
nonce,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
authorizeState,
|
||||
prompt
|
||||
} = data;
|
||||
|
||||
let isLoading = $state(false);
|
||||
let success = $state(false);
|
||||
@@ -30,7 +38,26 @@
|
||||
let authorizationConfirmed = $state(false);
|
||||
let userSignedInAt: Date | undefined;
|
||||
|
||||
// Parse prompt parameter once (space-delimited per OIDC spec)
|
||||
const promptValues = prompt ? prompt.split(' ') : [];
|
||||
const hasPromptNone = promptValues.includes('none');
|
||||
const hasPromptConsent = promptValues.includes('consent');
|
||||
const hasPromptLogin = promptValues.includes('login');
|
||||
const hasPromptSelectAccount = promptValues.includes('select_account');
|
||||
|
||||
onMount(() => {
|
||||
// Conflicting prompt values - none can't be combined with any interactive prompt
|
||||
if (hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount)) {
|
||||
redirectWithError('interaction_required');
|
||||
return;
|
||||
}
|
||||
|
||||
// If prompt=none and user is not signed in, redirect immediately with login_required
|
||||
if (hasPromptNone && !$userStore) {
|
||||
redirectWithError('login_required');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($userStore) {
|
||||
authorize();
|
||||
}
|
||||
@@ -52,6 +79,18 @@
|
||||
|
||||
if (!authorizationConfirmed) {
|
||||
authorizationRequired = await oidService.isAuthorizationRequired(client!.id, scope);
|
||||
|
||||
// If prompt=consent, always show consent UI
|
||||
if (hasPromptConsent) {
|
||||
authorizationRequired = true;
|
||||
}
|
||||
|
||||
// If prompt=none and consent required, redirect with error
|
||||
if (hasPromptNone && authorizationRequired) {
|
||||
redirectWithError('consent_required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (authorizationRequired) {
|
||||
isLoading = false;
|
||||
authorizationConfirmed = true;
|
||||
@@ -60,7 +99,7 @@
|
||||
}
|
||||
|
||||
let reauthToken: string | undefined;
|
||||
if (client?.requiresReauthentication) {
|
||||
if (client?.requiresReauthentication || hasPromptLogin) {
|
||||
let authResponse;
|
||||
const signedInRecently =
|
||||
userSignedInAt && userSignedInAt.getTime() > Date.now() - 60 * 1000;
|
||||
@@ -71,33 +110,60 @@
|
||||
reauthToken = await webauthnService.reauthenticate(authResponse);
|
||||
}
|
||||
|
||||
await oidService
|
||||
.authorize(
|
||||
client!.id,
|
||||
scope,
|
||||
callbackURL,
|
||||
nonce,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
reauthToken
|
||||
)
|
||||
.then(async ({ code, callbackURL, issuer }) => {
|
||||
onSuccess(code, callbackURL, issuer);
|
||||
});
|
||||
const result = await oidService.authorize(
|
||||
client!.id,
|
||||
scope,
|
||||
callbackURL,
|
||||
nonce,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
reauthToken,
|
||||
prompt
|
||||
);
|
||||
|
||||
// Check if backend returned a redirect error
|
||||
if (result.requiresRedirect && result.error) {
|
||||
if (hasPromptNone) {
|
||||
redirectWithError(result.error);
|
||||
} else {
|
||||
errorMessage = result.error;
|
||||
isLoading = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess(result.code!, result.callbackURL!, result.issuer!);
|
||||
} catch (e) {
|
||||
errorMessage = getWebauthnErrorMessage(e);
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function redirectWithError(error: string) {
|
||||
const redirectURL = new URL(callbackURL);
|
||||
if (redirectURL.protocol == 'javascript:' || redirectURL.protocol == 'data:') {
|
||||
throw new Error('Invalid redirect URL protocol');
|
||||
}
|
||||
|
||||
redirectURL.searchParams.append('error', error);
|
||||
if (authorizeState) {
|
||||
redirectURL.searchParams.append('state', authorizeState);
|
||||
}
|
||||
window.location.href = redirectURL.toString();
|
||||
}
|
||||
|
||||
function onSuccess(code: string, callbackURL: string, issuer: string) {
|
||||
const redirectURL = new URL(callbackURL);
|
||||
if (redirectURL.protocol == 'javascript:' || redirectURL.protocol == 'data:') {
|
||||
throw new Error('Invalid redirect URL protocol');
|
||||
}
|
||||
|
||||
redirectURL.searchParams.append('code', code);
|
||||
redirectURL.searchParams.append('state', authorizeState);
|
||||
redirectURL.searchParams.append('iss', issuer);
|
||||
|
||||
success = true;
|
||||
setTimeout(() => {
|
||||
const redirectURL = new URL(callbackURL);
|
||||
redirectURL.searchParams.append('code', code);
|
||||
redirectURL.searchParams.append('state', authorizeState);
|
||||
redirectURL.searchParams.append('iss', issuer);
|
||||
|
||||
window.location.href = redirectURL.toString();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const load: PageLoad = async ({ url }) => {
|
||||
callbackURL: url.searchParams.get('redirect_uri')!,
|
||||
client,
|
||||
codeChallenge: url.searchParams.get('code_challenge')!,
|
||||
codeChallengeMethod: url.searchParams.get('code_challenge_method')!
|
||||
codeChallengeMethod: url.searchParams.get('code_challenge_method')!,
|
||||
prompt: url.searchParams.get('prompt') || undefined
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<div class="flex justify-center gap-3">
|
||||
<div
|
||||
class=" bg-muted transition-translate rounded-2xl p-3 duration-500 ease-in {success || error
|
||||
class=" bg-muted transition-transform rounded-2xl p-3 duration-500 ease-in {success || error
|
||||
? 'translate-x-[108px]'
|
||||
: ''}"
|
||||
>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
const currentLocale = getLocale();
|
||||
|
||||
const locales = {
|
||||
ca: 'Català',
|
||||
cs: 'Čeština',
|
||||
da: 'Dansk',
|
||||
de: 'Deutsch',
|
||||
|
||||
@@ -70,10 +70,10 @@
|
||||
|
||||
const backgroundImagePromise =
|
||||
backgroundImage === null
|
||||
? appConfigService.deleteBackgroundImage()
|
||||
: backgroundImage
|
||||
? appConfigService.updateBackgroundImage(backgroundImage)
|
||||
: Promise.resolve();
|
||||
? appConfigService.deleteBackgroundImage()
|
||||
: backgroundImage
|
||||
? appConfigService.updateBackgroundImage(backgroundImage)
|
||||
: Promise.resolve();
|
||||
|
||||
await Promise.all([
|
||||
lightLogoPromise,
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
/>
|
||||
</div>
|
||||
<h4 class="mt-10 text-lg font-semibold">{m.attribute_mapping()}</h4>
|
||||
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
||||
<div class="mt-4 grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<FormInput
|
||||
label={m.user_unique_identifier_attribute()}
|
||||
description={m.the_value_of_this_attribute_should_never_change()}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
data-testid={`callback-url-${i + 1}`}
|
||||
type="text"
|
||||
inputmode="url"
|
||||
autocomplete="url"
|
||||
autocomplete="url"
|
||||
bind:value={callbackURLs[i]}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -5,23 +5,27 @@
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Item from '$lib/components/ui/item/index.js';
|
||||
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import type { UserCreate } from '$lib/types/user.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideChevronLeft } from '@lucide/svelte';
|
||||
import { KeyRound, LucideChevronLeft } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { backNavigate } from '../navigate-back-util';
|
||||
import UserForm from '../user-form.svelte';
|
||||
import AdminPasskeyList from './admin-passkey-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let user = $state({
|
||||
...data.user,
|
||||
userGroupIds: data.user.userGroups.map((g) => g.id)
|
||||
});
|
||||
let passkeys: Passkey[] = $state(data.passkeys);
|
||||
|
||||
const userService = new UserService();
|
||||
const customClaimService = new CustomClaimService();
|
||||
@@ -128,6 +132,25 @@
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
<Item.Group class="bg-card rounded-xl border p-4 shadow-sm">
|
||||
<Item.Root class="border-none bg-transparent p-0">
|
||||
<Item.Media class="text-primary/80">
|
||||
<KeyRound class="size-5" />
|
||||
</Item.Media>
|
||||
<Item.Content class="min-w-52">
|
||||
<Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
|
||||
<Item.Description
|
||||
>{passkeys.length > 0
|
||||
? m.manage_this_users_passkeys()
|
||||
: m.user_has_no_passkeys_yet()}</Item.Description
|
||||
>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
{#if passkeys.length > 0}
|
||||
<AdminPasskeyList userId={user.id} bind:passkeys />
|
||||
{/if}
|
||||
</Item.Group>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-custom-claims"
|
||||
title={m.custom_claims()}
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const userService = new UserService();
|
||||
const user = await userService.get(params.id);
|
||||
const [user, passkeys] = await Promise.all([
|
||||
userService.get(params.id),
|
||||
userService.listUserPasskeys(params.id)
|
||||
]);
|
||||
|
||||
return {
|
||||
user
|
||||
user,
|
||||
passkeys
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import PasskeyRow from '$lib/components/passkey-row.svelte';
|
||||
import * as Item from '$lib/components/ui/item/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideKeyRound } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
userId,
|
||||
passkeys = $bindable()
|
||||
}: {
|
||||
userId: string;
|
||||
passkeys: Passkey[];
|
||||
} = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
async function refreshPasskeys() {
|
||||
passkeys = await userService.listUserPasskeys(userId);
|
||||
}
|
||||
|
||||
function deletePasskey(passkey: Passkey) {
|
||||
openConfirmDialog({
|
||||
title: m.delete_passkey_name({ passkeyName: passkey.name }),
|
||||
message: m.are_you_sure_you_want_to_delete_this_passkey(),
|
||||
confirm: {
|
||||
label: m.delete(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await userService.removeUserPasskey(userId, passkey.id);
|
||||
await refreshPasskeys();
|
||||
toast.success(m.passkey_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Item.Group class="mt-3">
|
||||
{#each [...passkeys].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) as passkey}
|
||||
<PasskeyRow
|
||||
label={passkey.name}
|
||||
description={m.added_on() + ' ' + new Date(passkey.createdAt).toLocaleDateString()}
|
||||
icon={LucideKeyRound}
|
||||
showRenameAction={false}
|
||||
onDelete={() => deletePasskey(passkey)}
|
||||
/>
|
||||
{/each}
|
||||
</Item.Group>
|
||||
18
frontend/src/routes/signup/setup/+page.ts
Normal file
18
frontend/src/routes/signup/setup/+page.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { AxiosError } from 'axios';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const userService = new UserService();
|
||||
|
||||
try {
|
||||
await userService.checkInitialUserSetupAvailable();
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError && e.response?.status === 404) {
|
||||
error(404, 'Not found');
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -21,7 +21,7 @@ export default defineConfig((mode) => {
|
||||
disable: mode.isPreview,
|
||||
algorithm: 'gzip',
|
||||
ext: '.gz',
|
||||
filter: /\.(js|mjs|json|css)$/i
|
||||
filter: /\.(js|mjs|json|css)$/i
|
||||
}),
|
||||
|
||||
// Create brotli-compressed files
|
||||
@@ -29,7 +29,7 @@ export default defineConfig((mode) => {
|
||||
disable: mode.isPreview,
|
||||
algorithm: 'brotliCompress',
|
||||
ext: '.br',
|
||||
filter: /\.(js|mjs|json|css)$/i
|
||||
filter: /\.(js|mjs|json|css)$/i
|
||||
})
|
||||
],
|
||||
|
||||
|
||||
2544
pnpm-lock.yaml
generated
2544
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user