mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-10 23:33:00 +03:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04d8500910 | ||
|
|
93639dddb2 | ||
|
|
a190529117 | ||
|
|
73392b5837 | ||
|
|
65616f65e5 | ||
|
|
98a99fbb0a | ||
|
|
3f3b6b88fd | ||
|
|
8f98d8c0b4 | ||
|
|
c9308472a9 | ||
|
|
6362ff9861 | ||
|
|
10d640385f | ||
|
|
47927d1574 | ||
|
|
b356cef766 | ||
|
|
9fc45930a8 | ||
|
|
028d1c858e | ||
|
|
eb3963d0fc | ||
|
|
35d913f905 | ||
|
|
32485f4c7c | ||
|
|
ceb38b0825 | ||
|
|
c0b6ede5be | ||
|
|
c20e93b55c | ||
|
|
24ca6a106d | ||
|
|
9f0aa55be6 | ||
|
|
068fcc65a6 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,12 @@
|
||||
# JetBrains
|
||||
**/.idea
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
|
||||
# PNPM
|
||||
.pnpm-store/
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,3 +1,50 @@
|
||||
## v1.14.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- dark oidc client icons not saved on client creation ([#1057](https://github.com/pocket-id/pocket-id/pull/1057) by @mufeedali)
|
||||
|
||||
### Other
|
||||
|
||||
- add Turkish language files ([a190529](https://github.com/pocket-id/pocket-id/commit/a190529117fe20b5b836d452b382da69abba9458) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.14.1...v1.14.2
|
||||
|
||||
## v1.14.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Prevent blinding FOUC in dark mode ([#1054](https://github.com/pocket-id/pocket-id/pull/1054) by @mufeedali)
|
||||
- use credProps to save passkey on firefox android ([#1055](https://github.com/pocket-id/pocket-id/pull/1055) by @lhoursquentin)
|
||||
- ignore trailing slashes in `APP_URL` ([65616f6](https://github.com/pocket-id/pocket-id/commit/65616f65e53f3e62d18a8209929e68ddc8d2b9b8) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.14.0...v1.14.1
|
||||
|
||||
## v1.14.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- ignore trailing slash in URL ([9f0aa55](https://github.com/pocket-id/pocket-id/commit/9f0aa55be67b7a09810569250563bb388b40590a) by @stonith404)
|
||||
- use constant time comparisons when validating PKCE challenges ([#1047](https://github.com/pocket-id/pocket-id/pull/1047) by @ItalyPaleAle)
|
||||
- only animate login background on initial page load ([b356cef](https://github.com/pocket-id/pocket-id/commit/b356cef766697c621157235ae1d2743f3fe6720d) by @stonith404)
|
||||
- make pkce requirement visible in the oidc form if client is public ([47927d1](https://github.com/pocket-id/pocket-id/commit/47927d157470daa5b5a5b30e61a2ba69110eeff9) by @stonith404)
|
||||
- prevent page flickering on redirection based on auth state ([10d6403](https://github.com/pocket-id/pocket-id/commit/10d640385ff2078299a07f05e5ca3f0d392eecf7) by @stonith404)
|
||||
|
||||
### Features
|
||||
|
||||
- add various improvements to the table component ([#961](https://github.com/pocket-id/pocket-id/pull/961) by @stonith404)
|
||||
- add support for dark mode oidc client icons ([#1039](https://github.com/pocket-id/pocket-id/pull/1039) by @kmendell)
|
||||
|
||||
### Other
|
||||
|
||||
- add Japanese files ([068fcc6](https://github.com/pocket-id/pocket-id/commit/068fcc65a62c76f55c9636f830fc769bd59220c4) by @kmendell)
|
||||
- bump sveltekit-superforms from 2.27.1 to 2.27.4 in the npm_and_yarn group across 1 directory ([#1031](https://github.com/pocket-id/pocket-id/pull/1031) by @dependabot[bot])
|
||||
- update AAGUIDs ([#1041](https://github.com/pocket-id/pocket-id/pull/1041) by @github-actions[bot])
|
||||
- bump vite from 7.0.7 to 7.0.8 in the npm_and_yarn group across 1 directory ([#1042](https://github.com/pocket-id/pocket-id/pull/1042) by @dependabot[bot])
|
||||
- upgrade dependencies ([6362ff9](https://github.com/pocket-id/pocket-id/commit/6362ff986124d056cc07d214855f198eab9cb97d) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.13.1...v1.14.0
|
||||
|
||||
## v1.13.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -32,10 +32,6 @@ func init() {
|
||||
panic(fmt.Errorf("failed to read index.html: %w", iErr))
|
||||
}
|
||||
|
||||
// Get the position of the first <script> tag
|
||||
idx := bytes.Index(index, []byte(scriptTag))
|
||||
|
||||
// Create writeIndexFn, which adds the CSP tag to the script tag if needed
|
||||
writeIndexFn = func(w io.Writer, nonce string) (err error) {
|
||||
// If there's no nonce, write the index as-is
|
||||
if nonce == "" {
|
||||
@@ -43,23 +39,16 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
// We have a nonce, so first write the index until the <script> tag
|
||||
// Then we write the modified script tag
|
||||
// Finally, the rest of the index
|
||||
_, err = w.Write(index[0:idx])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte(`<script nonce="` + nonce + `">`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(index[(idx + len(scriptTag)):])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add nonce to all <script> tags
|
||||
// We replace "<script" with `<script nonce="..."` everywhere it appears
|
||||
modified := bytes.ReplaceAll(
|
||||
index,
|
||||
[]byte(scriptTag),
|
||||
[]byte(`<script nonce="`+nonce+`">`),
|
||||
)
|
||||
|
||||
return nil
|
||||
_, err = w.Write(modified)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +64,11 @@ func RegisterFrontend(router *gin.Engine) error {
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||
|
||||
if strings.HasSuffix(path, "/") {
|
||||
c.Redirect(http.StatusMovedPermanently, strings.TrimRight(c.Request.URL.String(), "/"))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "api/") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
|
||||
return
|
||||
@@ -94,13 +88,9 @@ func RegisterFrontend(router *gin.Engine) error {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Status(http.StatusOK)
|
||||
|
||||
err = writeIndexFn(c.Writer, nonce)
|
||||
if err != nil {
|
||||
if err := writeIndexFn(c.Writer, nonce); err != nil {
|
||||
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
156
backend/go.mod
156
backend/go.mod
@@ -7,84 +7,87 @@ require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||
github.com/emersion/go-smtp v0.21.3
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||
github.com/emersion/go-smtp v0.24.0
|
||||
github.com/fxamacker/cbor/v2 v2.9.0
|
||||
github.com/gin-contrib/slog v1.1.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.3
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
github.com/golang-migrate/migrate/v4 v4.18.3
|
||||
github.com/go-co-op/gocron/v2 v2.17.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/go-webauthn/webauthn v0.14.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12
|
||||
github.com/lmittmann/tint v1.1.2
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/log v0.13.0
|
||||
go.opentelemetry.io/otel/metric v1.37.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.10.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/text v0.28.0
|
||||
golang.org/x/time v0.12.0
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/log v0.14.0
|
||||
go.opentelemetry.io/otel/metric v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/image v0.32.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/text v0.30.0
|
||||
golang.org/x/time v0.14.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.1
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/disintegration/gift v1.1.2 // indirect
|
||||
github.com/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/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // 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-webauthn/x v0.1.23 // indirect
|
||||
github.com/go-webauthn/x v0.1.25 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-github/v39 v39.2.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@@ -93,57 +96,66 @@ 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-secp256k1 v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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 v0.1.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // 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.1 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.18.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.55.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.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
modernc.org/sqlite v1.39.1 // indirect
|
||||
)
|
||||
|
||||
176
backend/go.sum
176
backend/go.sum
@@ -6,10 +6,15 @@ 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-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
@@ -30,8 +35,11 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
|
||||
github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
|
||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
||||
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
@@ -40,6 +48,7 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
||||
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -48,30 +57,44 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
|
||||
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4=
|
||||
github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/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/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-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.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
||||
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
||||
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
|
||||
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
|
||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -85,18 +108,30 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
|
||||
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
|
||||
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
|
||||
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
|
||||
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
|
||||
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -112,6 +147,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
@@ -119,8 +156,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -137,6 +178,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -177,12 +220,20 @@ 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-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.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||
@@ -195,6 +246,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -214,6 +267,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 v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/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=
|
||||
@@ -222,6 +277,8 @@ 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.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0 h1:Gyljxck1kHbBxDgLM++NfDWBqvu1pWWfT8XbosSo0bo=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -230,25 +287,47 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
|
||||
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -261,6 +340,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
@@ -272,60 +353,116 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
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.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 h1:HY2hJ7yn3KuEBBBsKxvF3ViSmzLwsgeNvD+0utRMgzc=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0/go.mod h1:H4H7vs8766kwFnOZVEGMJFVF+phpBSmTckvvNRdJeDI=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0/go.mod h1:fPl+qlrhRdRntIpPs9JoQ0iBKAsnH5VkgppU1f9kyF4=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 h1:5kSIJ0y8ckZZKoDhZHdVtcyjVi6rXyAwyaR8mp4zLbg=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0/go.mod h1:i+fIMHvcSQtsIY82/xgiVWRklrNt/O6QriHLjzGeY+s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
||||
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
|
||||
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
|
||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw=
|
||||
go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -336,11 +473,17 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -348,6 +491,8 @@ 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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
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-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -362,9 +507,13 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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=
|
||||
@@ -374,6 +523,8 @@ 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -388,6 +539,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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=
|
||||
@@ -409,8 +562,12 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@@ -419,17 +576,27 @@ 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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
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=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -441,18 +608,25 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
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=
|
||||
@@ -463,6 +637,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
||||
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -35,7 +35,7 @@ const (
|
||||
type EnvConfigSchema struct {
|
||||
AppEnv string `env:"APP_ENV" options:"toLower"`
|
||||
LogLevel string `env:"LOG_LEVEL" options:"toLower"`
|
||||
AppURL string `env:"APP_URL" options:"toLower"`
|
||||
AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"`
|
||||
DbProvider DbProvider `env:"DB_PROVIDER" options:"toLower"`
|
||||
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
|
||||
UploadPath string `env:"UPLOAD_PATH"`
|
||||
@@ -227,6 +227,10 @@ func prepareEnvConfig(config *EnvConfigSchema) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "trimTrailingSlash":
|
||||
if field.Kind() == reflect.String {
|
||||
field.SetString(strings.TrimRight(field.String(), "/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +45,11 @@ func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.Auth
|
||||
// @Success 200 {object} dto.Paginated[dto.ApiKeyDto]
|
||||
// @Router /api/api-keys [get]
|
||||
func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
|
||||
listRequestOptions := utils.ParseListRequestOptions(ctx)
|
||||
|
||||
userID := ctx.GetString("userID")
|
||||
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := ctx.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiKeys, pagination, err := c.apiKeyService.ListApiKeys(ctx.Request.Context(), userID, sortedPaginationRequest)
|
||||
apiKeys, pagination, err := c.apiKeyService.ListApiKeys(ctx.Request.Context(), userID, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
|
||||
@@ -41,18 +41,12 @@ type AuditLogController struct {
|
||||
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
||||
// @Router /api/audit-logs [get]
|
||||
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
|
||||
err := c.ShouldBindQuery(&sortedPaginationRequest)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
// Fetch audit logs for the user
|
||||
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, sortedPaginationRequest)
|
||||
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -86,26 +80,12 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @Param sort[column] query string false "Column to sort by"
|
||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Param filters[userId] query string false "Filter by user ID"
|
||||
// @Param filters[event] query string false "Filter by event type"
|
||||
// @Param filters[clientName] query string false "Filter by client name"
|
||||
// @Param filters[location] query string false "Filter by location type (external or internal)"
|
||||
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
||||
// @Router /api/audit-logs/all [get]
|
||||
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
var filters dto.AuditLogFilterDto
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), sortedPaginationRequest, filters)
|
||||
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -357,6 +358,7 @@ func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
|
||||
clientDto := dto.OidcClientMetaDataDto{}
|
||||
err = dto.MapStruct(client, &clientDto)
|
||||
if err == nil {
|
||||
clientDto.HasDarkLogo = client.HasDarkLogo()
|
||||
c.JSON(http.StatusOK, clientDto)
|
||||
return
|
||||
}
|
||||
@@ -403,13 +405,9 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
||||
// @Router /api/oidc/clients [get]
|
||||
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, sortedPaginationRequest)
|
||||
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -423,6 +421,7 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
clientDto.HasDarkLogo = client.HasDarkLogo()
|
||||
clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
@@ -543,10 +542,13 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
||||
// @Produce image/jpeg
|
||||
// @Produce image/svg+xml
|
||||
// @Param id path string true "Client ID"
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Success 200 {file} binary "Logo image"
|
||||
// @Router /api/oidc/clients/{id}/logo [get]
|
||||
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"))
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
|
||||
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -565,6 +567,7 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||
// @Accept multipart/form-data
|
||||
// @Param id path string true "Client ID"
|
||||
// @Param file formData file true "Logo image file (PNG, JPG, or SVG)"
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/oidc/clients/{id}/logo [post]
|
||||
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||
@@ -574,13 +577,16 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file)
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
|
||||
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file, lightLogo)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
|
||||
}
|
||||
|
||||
// deleteClientLogoHandler godoc
|
||||
@@ -588,16 +594,26 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||
// @Description Delete the logo for an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Param id path string true "Client ID"
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/oidc/clients/{id}/logo [delete]
|
||||
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
||||
err := oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id"))
|
||||
var err error
|
||||
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
if lightLogo {
|
||||
err = oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id"))
|
||||
} else {
|
||||
err = oc.oidcService.DeleteClientDarkLogo(c.Request.Context(), c.Param("id"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
|
||||
}
|
||||
|
||||
// updateAllowedUserGroupsHandler godoc
|
||||
@@ -628,6 +644,7 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
oidcClientDto.HasDarkLogo = oidcClient.HasDarkLogo()
|
||||
|
||||
c.JSON(http.StatusOK, oidcClientDto)
|
||||
}
|
||||
@@ -685,12 +702,9 @@ func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, sortedPaginationRequest)
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -741,15 +755,11 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
|
||||
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
|
||||
// @Router /api/oidc/users/me/clients [get]
|
||||
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, sortedPaginationRequest)
|
||||
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -104,13 +104,9 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||
// @Router /api/users [get]
|
||||
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, sortedPaginationRequest)
|
||||
users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -574,13 +570,9 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
|
||||
// @Success 200 {object} dto.Paginated[dto.SignupTokenDto]
|
||||
// @Router /api/signup-tokens [get]
|
||||
func (uc *UserController) listSignupTokensHandler(c *gin.Context) {
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
tokens, pagination, err := uc.userService.ListSignupTokens(c.Request.Context(), sortedPaginationRequest)
|
||||
tokens, pagination, err := uc.userService.ListSignupTokens(c.Request.Context(), listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -47,16 +47,10 @@ type UserGroupController struct {
|
||||
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
|
||||
// @Router /api/user-groups [get]
|
||||
func (ugc *UserGroupController) list(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
searchTerm := c.Query("search")
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
groups, pagination, err := ugc.UserGroupService.List(ctx, searchTerm, sortedPaginationRequest)
|
||||
groups, pagination, err := ugc.UserGroupService.List(c, searchTerm, listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -70,7 +64,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(ctx, group.ID)
|
||||
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(c.Request.Context(), group.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -17,10 +17,3 @@ type AuditLogDto struct {
|
||||
Username string `json:"username"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
type AuditLogFilterDto struct {
|
||||
UserID string `form:"filters[userId]"`
|
||||
Event string `form:"filters[event]"`
|
||||
ClientName string `form:"filters[clientName]"`
|
||||
Location string `form:"filters[location]"`
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ type OidcClientMetaDataDto struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||
LaunchURL *string `json:"launchURL"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
}
|
||||
@@ -39,7 +40,9 @@ type OidcClientUpdateDto struct {
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||
LogoURL *string `json:"logoUrl"`
|
||||
DarkLogoURL *string `json:"darkLogoUrl"`
|
||||
}
|
||||
|
||||
type OidcClientCreateDto struct {
|
||||
|
||||
@@ -3,13 +3,14 @@ package model
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
type AuditLog struct {
|
||||
Base
|
||||
|
||||
Event AuditLogEvent `sortable:"true"`
|
||||
Event AuditLogEvent `sortable:"true" filterable:"true"`
|
||||
IpAddress *string `sortable:"true"`
|
||||
Country string `sortable:"true"`
|
||||
City string `sortable:"true"`
|
||||
@@ -17,7 +18,7 @@ type AuditLog struct {
|
||||
Username string `gorm:"-"`
|
||||
Data AuditLogData
|
||||
|
||||
UserID string
|
||||
UserID string `filterable:"true"`
|
||||
User User
|
||||
}
|
||||
|
||||
@@ -47,14 +48,7 @@ func (e AuditLogEvent) Value() (driver.Value, error) {
|
||||
}
|
||||
|
||||
func (d *AuditLogData) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, d)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), d)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", value)
|
||||
}
|
||||
return utils.UnmarshalJSONFromDatabase(d, value)
|
||||
}
|
||||
|
||||
func (d AuditLogData) Value() (driver.Value, error) {
|
||||
|
||||
@@ -3,10 +3,10 @@ package model
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
type UserAuthorizedOidcClient struct {
|
||||
@@ -52,9 +52,10 @@ type OidcClient struct {
|
||||
CallbackURLs UrlList
|
||||
LogoutCallbackURLs UrlList
|
||||
ImageType *string
|
||||
DarkImageType *string
|
||||
IsPublic bool
|
||||
PkceEnabled bool
|
||||
RequiresReauthentication bool
|
||||
PkceEnabled bool `filterable:"true"`
|
||||
RequiresReauthentication bool `filterable:"true"`
|
||||
Credentials OidcClientCredentials
|
||||
LaunchURL *string
|
||||
|
||||
@@ -68,6 +69,10 @@ func (c OidcClient) HasLogo() bool {
|
||||
return c.ImageType != nil && *c.ImageType != ""
|
||||
}
|
||||
|
||||
func (c OidcClient) HasDarkLogo() bool {
|
||||
return c.DarkImageType != nil && *c.DarkImageType != ""
|
||||
}
|
||||
|
||||
type OidcRefreshToken struct {
|
||||
Base
|
||||
|
||||
@@ -116,14 +121,7 @@ func (occ OidcClientCredentials) FederatedIdentityForIssuer(issuer string) (Oidc
|
||||
}
|
||||
|
||||
func (occ *OidcClientCredentials) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, occ)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), occ)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", value)
|
||||
}
|
||||
return utils.UnmarshalJSONFromDatabase(occ, value)
|
||||
}
|
||||
|
||||
func (occ OidcClientCredentials) Value() (driver.Value, error) {
|
||||
@@ -133,14 +131,7 @@ func (occ OidcClientCredentials) Value() (driver.Value, error) {
|
||||
type UrlList []string //nolint:recvcheck
|
||||
|
||||
func (cu *UrlList) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, cu)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), cu)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", value)
|
||||
}
|
||||
return utils.UnmarshalJSONFromDatabase(cu, value)
|
||||
}
|
||||
|
||||
func (cu UrlList) Value() (driver.Value, error) {
|
||||
|
||||
@@ -18,10 +18,10 @@ type User struct {
|
||||
FirstName string `sortable:"true"`
|
||||
LastName string `sortable:"true"`
|
||||
DisplayName string `sortable:"true"`
|
||||
IsAdmin bool `sortable:"true"`
|
||||
IsAdmin bool `sortable:"true" filterable:"true"`
|
||||
Locale *string
|
||||
LdapID *string
|
||||
Disabled bool `sortable:"true"`
|
||||
Disabled bool `sortable:"true" filterable:"true"`
|
||||
|
||||
CustomClaims []CustomClaim
|
||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||
|
||||
@@ -3,11 +3,11 @@ package model
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
type WebauthnSession struct {
|
||||
@@ -16,6 +16,7 @@ type WebauthnSession struct {
|
||||
Challenge string
|
||||
ExpiresAt datatype.DateTime
|
||||
UserVerification string
|
||||
CredentialParams CredentialParameters
|
||||
}
|
||||
|
||||
type WebauthnCredential struct {
|
||||
@@ -58,16 +59,20 @@ type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvc
|
||||
|
||||
// Scan and Value methods for GORM to handle the custom type
|
||||
func (atl *AuthenticatorTransportList) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, atl)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), atl)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", value)
|
||||
}
|
||||
return utils.UnmarshalJSONFromDatabase(atl, value)
|
||||
}
|
||||
|
||||
func (atl AuthenticatorTransportList) Value() (driver.Value, error) {
|
||||
return json.Marshal(atl)
|
||||
}
|
||||
|
||||
type CredentialParameters []protocol.CredentialParameter //nolint:recvcheck
|
||||
|
||||
// Scan and Value methods for GORM to handle the custom type
|
||||
func (cp *CredentialParameters) Scan(value interface{}) error {
|
||||
return utils.UnmarshalJSONFromDatabase(cp, value)
|
||||
}
|
||||
|
||||
func (cp CredentialParameters) Value() (driver.Value, error) {
|
||||
return json.Marshal(cp)
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ func NewApiKeyService(db *gorm.DB, emailService *EmailService) *ApiKeyService {
|
||||
return &ApiKeyService{db: db, emailService: emailService}
|
||||
}
|
||||
|
||||
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.ApiKey, utils.PaginationResponse, error) {
|
||||
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.ApiKey, utils.PaginationResponse, error) {
|
||||
query := s.db.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Model(&model.ApiKey{})
|
||||
|
||||
var apiKeys []model.ApiKey
|
||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &apiKeys)
|
||||
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &apiKeys)
|
||||
if err != nil {
|
||||
return nil, utils.PaginationResponse{}, err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"log/slog"
|
||||
|
||||
userAgentParser "github.com/mileusna/useragent"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||
@@ -136,14 +135,14 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
}
|
||||
|
||||
// ListAuditLogsForUser retrieves all audit logs for a given user ID
|
||||
func (s *AuditLogService) ListAuditLogsForUser(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||
func (s *AuditLogService) ListAuditLogsForUser(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||
var logs []model.AuditLog
|
||||
query := s.db.
|
||||
WithContext(ctx).
|
||||
Model(&model.AuditLog{}).
|
||||
Where("user_id = ?", userID)
|
||||
|
||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
|
||||
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &logs)
|
||||
return logs, pagination, err
|
||||
}
|
||||
|
||||
@@ -152,7 +151,7 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
|
||||
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
||||
}
|
||||
|
||||
func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPaginationRequest utils.SortedPaginationRequest, filters dto.AuditLogFilterDto) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||
func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||
var logs []model.AuditLog
|
||||
|
||||
query := s.db.
|
||||
@@ -160,33 +159,36 @@ func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPagination
|
||||
Preload("User").
|
||||
Model(&model.AuditLog{})
|
||||
|
||||
if filters.UserID != "" {
|
||||
query = query.Where("user_id = ?", filters.UserID)
|
||||
}
|
||||
if filters.Event != "" {
|
||||
query = query.Where("event = ?", filters.Event)
|
||||
}
|
||||
if filters.ClientName != "" {
|
||||
if clientName, ok := listRequestOptions.Filters["clientName"]; ok {
|
||||
dialect := s.db.Name()
|
||||
switch dialect {
|
||||
case "sqlite":
|
||||
query = query.Where("json_extract(data, '$.clientName') = ?", filters.ClientName)
|
||||
query = query.Where("json_extract(data, '$.clientName') IN ?", clientName)
|
||||
case "postgres":
|
||||
query = query.Where("data->>'clientName' = ?", filters.ClientName)
|
||||
query = query.Where("data->>'clientName' IN ?", clientName)
|
||||
default:
|
||||
return nil, utils.PaginationResponse{}, fmt.Errorf("unsupported database dialect: %s", dialect)
|
||||
}
|
||||
}
|
||||
if filters.Location != "" {
|
||||
switch filters.Location {
|
||||
case "external":
|
||||
query = query.Where("country != 'Internal Network'")
|
||||
case "internal":
|
||||
query = query.Where("country = 'Internal Network'")
|
||||
|
||||
if locations, ok := listRequestOptions.Filters["location"]; ok {
|
||||
mapped := make([]string, 0, len(locations))
|
||||
for _, v := range locations {
|
||||
if s, ok := v.(string); ok {
|
||||
switch s {
|
||||
case "internal":
|
||||
mapped = append(mapped, "Internal Network")
|
||||
case "external":
|
||||
mapped = append(mapped, "External Network")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(mapped) > 0 {
|
||||
query = query.Where("country IN ?", mapped)
|
||||
}
|
||||
}
|
||||
|
||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
|
||||
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &logs)
|
||||
if err != nil {
|
||||
return nil, pagination, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -394,7 +395,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, inpu
|
||||
|
||||
// If the client is public or PKCE is enabled, the code verifier must match the code challenge
|
||||
if client.IsPublic || client.PkceEnabled {
|
||||
if !s.validateCodeVerifier(input.CodeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
||||
if !validateCodeVerifier(input.CodeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
||||
return CreatedTokens{}, &common.OidcInvalidCodeVerifierError{}
|
||||
}
|
||||
}
|
||||
@@ -692,7 +693,7 @@ func (s *OidcService) getClientInternal(ctx context.Context, clientID string, tx
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) ListClients(ctx context.Context, name string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.OidcClient, utils.PaginationResponse, error) {
|
||||
func (s *OidcService) ListClients(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) ([]model.OidcClient, utils.PaginationResponse, error) {
|
||||
var clients []model.OidcClient
|
||||
|
||||
query := s.db.
|
||||
@@ -705,17 +706,17 @@ func (s *OidcService) ListClients(ctx context.Context, name string, sortedPagina
|
||||
}
|
||||
|
||||
// As allowedUserGroupsCount is not a column, we need to manually sort it
|
||||
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||
if listRequestOptions.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
|
||||
query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
|
||||
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
||||
Group("oidc_clients.id").
|
||||
Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + sortedPaginationRequest.Sort.Direction)
|
||||
Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + listRequestOptions.Sort.Direction)
|
||||
|
||||
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &clients)
|
||||
response, err := utils.Paginate(listRequestOptions.Pagination.Page, listRequestOptions.Pagination.Limit, query, &clients)
|
||||
return clients, response, err
|
||||
}
|
||||
|
||||
response, err := utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
|
||||
response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
|
||||
return clients, response, err
|
||||
}
|
||||
|
||||
@@ -745,12 +746,19 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
|
||||
}
|
||||
|
||||
if input.LogoURL != nil {
|
||||
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL)
|
||||
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL, true)
|
||||
if err != nil {
|
||||
return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input.DarkLogoURL != nil {
|
||||
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.DarkLogoURL, false)
|
||||
if err != nil {
|
||||
return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return model.OidcClient{}, err
|
||||
@@ -777,12 +785,19 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
||||
}
|
||||
|
||||
if input.LogoURL != nil {
|
||||
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL)
|
||||
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL, true)
|
||||
if err != nil {
|
||||
return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input.DarkLogoURL != nil {
|
||||
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.DarkLogoURL, false)
|
||||
if err != nil {
|
||||
return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return model.OidcClient{}, err
|
||||
}
|
||||
@@ -869,7 +884,7 @@ func (s *OidcService) CreateClientSecret(ctx context.Context, clientID string) (
|
||||
return clientSecret, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (string, string, error) {
|
||||
func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light bool) (string, string, error) {
|
||||
var client model.OidcClient
|
||||
err := s.db.
|
||||
WithContext(ctx).
|
||||
@@ -879,23 +894,38 @@ func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (strin
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if client.ImageType == nil {
|
||||
var imagePath, mimeType string
|
||||
|
||||
switch {
|
||||
case !light && client.DarkImageType != nil:
|
||||
// Dark logo if requested and exists
|
||||
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + *client.DarkImageType
|
||||
mimeType = utils.GetImageMimeType(*client.DarkImageType)
|
||||
|
||||
case client.ImageType != nil:
|
||||
// Light logo if requested or no dark logo is available
|
||||
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType
|
||||
mimeType = utils.GetImageMimeType(*client.ImageType)
|
||||
|
||||
default:
|
||||
return "", "", errors.New("image not found")
|
||||
}
|
||||
|
||||
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType
|
||||
mimeType := utils.GetImageMimeType(*client.ImageType)
|
||||
|
||||
return imagePath, mimeType, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader) error {
|
||||
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader, light bool) error {
|
||||
fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
|
||||
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
|
||||
return &common.FileTypeNotSupportedError{}
|
||||
}
|
||||
|
||||
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + "." + fileType
|
||||
var darkSuffix string
|
||||
if !light {
|
||||
darkSuffix = "-dark"
|
||||
}
|
||||
|
||||
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + darkSuffix + "." + fileType
|
||||
err := utils.SaveFile(file, imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -903,7 +933,7 @@ func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, fil
|
||||
|
||||
tx := s.db.Begin()
|
||||
|
||||
err = s.updateClientLogoType(ctx, tx, clientID, fileType)
|
||||
err = s.updateClientLogoType(ctx, tx, clientID, fileType, light)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -955,6 +985,49 @@ func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) DeleteClientDarkLogo(ctx context.Context, clientID string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var client model.OidcClient
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
First(&client, "id = ?", clientID).
|
||||
Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if client.DarkImageType == nil {
|
||||
return errors.New("image not found")
|
||||
}
|
||||
|
||||
oldImageType := *client.DarkImageType
|
||||
client.DarkImageType = nil
|
||||
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Save(&client).
|
||||
Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + oldImageType
|
||||
if err := os.Remove(imagePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
@@ -1083,13 +1156,20 @@ func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID stri
|
||||
return randomString, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
||||
func validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
||||
if codeVerifier == "" || codeChallenge == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !codeChallengeMethodSha256 {
|
||||
return codeVerifier == codeChallenge
|
||||
return subtle.ConstantTimeCompare([]byte(codeVerifier), []byte(codeChallenge)) == 1
|
||||
}
|
||||
|
||||
// Base64 URL decode the challenge
|
||||
// If it's not valid base64url, fail the operation
|
||||
codeChallengeBytes, err := base64.RawURLEncoding.DecodeString(codeChallenge)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compute SHA-256 hash of the codeVerifier
|
||||
@@ -1097,10 +1177,7 @@ func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, c
|
||||
h.Write([]byte(codeVerifier))
|
||||
codeVerifierHash := h.Sum(nil)
|
||||
|
||||
// Base64 URL encode the verifier hash
|
||||
encodedVerifierHash := base64.RawURLEncoding.EncodeToString(codeVerifierHash)
|
||||
|
||||
return encodedVerifierHash == codeChallenge
|
||||
return subtle.ConstantTimeCompare(codeVerifierHash, codeChallengeBytes) == 1
|
||||
}
|
||||
|
||||
func (s *OidcService) getCallbackURL(client *model.OidcClient, inputCallbackURL string, tx *gorm.DB, ctx context.Context) (callbackURL string, err error) {
|
||||
@@ -1324,9 +1401,10 @@ func (s *OidcService) GetDeviceCodeInfo(ctx context.Context, userCode string, us
|
||||
|
||||
return &dto.DeviceCodeInfoDto{
|
||||
Client: dto.OidcClientMetaDataDto{
|
||||
ID: deviceAuth.Client.ID,
|
||||
Name: deviceAuth.Client.Name,
|
||||
HasLogo: deviceAuth.Client.HasLogo(),
|
||||
ID: deviceAuth.Client.ID,
|
||||
Name: deviceAuth.Client.Name,
|
||||
HasLogo: deviceAuth.Client.HasLogo(),
|
||||
HasDarkLogo: deviceAuth.Client.HasDarkLogo(),
|
||||
},
|
||||
Scope: deviceAuth.Scope,
|
||||
AuthorizationRequired: !hasAuthorizedClient,
|
||||
@@ -1350,7 +1428,7 @@ func (s *OidcService) GetAllowedGroupsCountOfClient(ctx context.Context, id stri
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {
|
||||
func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {
|
||||
|
||||
query := s.db.
|
||||
WithContext(ctx).
|
||||
@@ -1359,7 +1437,7 @@ func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string,
|
||||
Where("user_id = ?", userID)
|
||||
|
||||
var authorizedClients []model.UserAuthorizedOidcClient
|
||||
response, err := utils.PaginateAndSort(sortedPaginationRequest, query, &authorizedClients)
|
||||
response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &authorizedClients)
|
||||
|
||||
return authorizedClients, response, err
|
||||
}
|
||||
@@ -1392,7 +1470,7 @@ func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
|
||||
func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -1439,13 +1517,13 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
||||
|
||||
// Handle custom sorting for lastUsedAt column
|
||||
var response utils.PaginationResponse
|
||||
if sortedPaginationRequest.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||
if listRequestOptions.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
|
||||
query = query.
|
||||
Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
|
||||
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction + " NULLS LAST")
|
||||
Order("user_authorized_oidc_clients.last_used_at " + listRequestOptions.Sort.Direction + " NULLS LAST")
|
||||
}
|
||||
|
||||
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
|
||||
response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
|
||||
if err != nil {
|
||||
return nil, utils.PaginationResponse{}, err
|
||||
}
|
||||
@@ -1458,10 +1536,11 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
||||
}
|
||||
dtos[i] = dto.AccessibleOidcClientDto{
|
||||
OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
|
||||
ID: client.ID,
|
||||
Name: client.Name,
|
||||
LaunchURL: client.LaunchURL,
|
||||
HasLogo: client.HasLogo(),
|
||||
ID: client.ID,
|
||||
Name: client.Name,
|
||||
LaunchURL: client.LaunchURL,
|
||||
HasLogo: client.HasLogo(),
|
||||
HasDarkLogo: client.HasDarkLogo(),
|
||||
},
|
||||
LastUsedAt: lastUsedAt,
|
||||
}
|
||||
@@ -1883,7 +1962,7 @@ func (s *OidcService) IsClientAccessibleToUser(ctx context.Context, clientID str
|
||||
return s.IsUserGroupAllowedToAuthorize(user, client), nil
|
||||
}
|
||||
|
||||
func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *gorm.DB, clientID string, raw string) error {
|
||||
func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *gorm.DB, clientID string, raw string, light bool) error {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1944,31 +2023,51 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *
|
||||
return err
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(folderPath, clientID+"."+ext)
|
||||
var darkSuffix string
|
||||
if !light {
|
||||
darkSuffix = "-dark"
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(folderPath, clientID+darkSuffix+"."+ext)
|
||||
err = utils.SaveFileStream(io.LimitReader(resp.Body, maxLogoSize+1), imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.updateClientLogoType(ctx, tx, clientID, ext); err != nil {
|
||||
if err := s.updateClientLogoType(ctx, tx, clientID, ext, light); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, clientID, ext string) error {
|
||||
func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, clientID, ext string, light bool) error {
|
||||
uploadsDir := common.EnvConfig.UploadPath + "/oidc-client-images"
|
||||
|
||||
var darkSuffix string
|
||||
if !light {
|
||||
darkSuffix = "-dark"
|
||||
}
|
||||
|
||||
var client model.OidcClient
|
||||
if err := tx.WithContext(ctx).First(&client, "id = ?", clientID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if client.ImageType != nil && *client.ImageType != ext {
|
||||
old := fmt.Sprintf("%s/%s.%s", uploadsDir, client.ID, *client.ImageType)
|
||||
old := fmt.Sprintf("%s/%s%s.%s", uploadsDir, client.ID, darkSuffix, *client.ImageType)
|
||||
_ = os.Remove(old)
|
||||
}
|
||||
client.ImageType = &ext
|
||||
return tx.WithContext(ctx).Save(&client).Error
|
||||
|
||||
var column string
|
||||
if light {
|
||||
column = "image_type"
|
||||
} else {
|
||||
column = "dark_image_type"
|
||||
}
|
||||
|
||||
return tx.WithContext(ctx).
|
||||
Model(&model.OidcClient{}).
|
||||
Where("id = ?", clientID).
|
||||
Update(column, ext).
|
||||
Error
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
@@ -510,3 +512,28 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateCodeVerifier_Plain(t *testing.T) {
|
||||
require.False(t, validateCodeVerifier("", "", false))
|
||||
require.False(t, validateCodeVerifier("", "", true))
|
||||
|
||||
t.Run("plain", func(t *testing.T) {
|
||||
require.False(t, validateCodeVerifier("", "challenge", false))
|
||||
require.False(t, validateCodeVerifier("verifier", "", false))
|
||||
require.True(t, validateCodeVerifier("plainVerifier", "plainVerifier", false))
|
||||
require.False(t, validateCodeVerifier("plainVerifier", "otherVerifier", false))
|
||||
})
|
||||
|
||||
t.Run("SHA 256", func(t *testing.T) {
|
||||
codeVerifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||
hash := sha256.Sum256([]byte(codeVerifier))
|
||||
codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
|
||||
|
||||
require.True(t, validateCodeVerifier(codeVerifier, codeChallenge, true))
|
||||
require.False(t, validateCodeVerifier("wrongVerifier", codeChallenge, true))
|
||||
require.False(t, validateCodeVerifier(codeVerifier, "!", true))
|
||||
|
||||
// Invalid base64
|
||||
require.False(t, validateCodeVerifier("NOT!VALID", codeChallenge, true))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService) *UserG
|
||||
return &UserGroupService{db: db, appConfigService: appConfigService}
|
||||
}
|
||||
|
||||
func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
||||
func (s *UserGroupService) List(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
||||
query := s.db.
|
||||
WithContext(ctx).
|
||||
Preload("CustomClaims").
|
||||
@@ -32,17 +32,14 @@ func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginati
|
||||
}
|
||||
|
||||
// As userCount is not a column we need to manually sort it
|
||||
if sortedPaginationRequest.Sort.Column == "userCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||
if listRequestOptions.Sort.Column == "userCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
|
||||
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
|
||||
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
|
||||
Group("user_groups.id").
|
||||
Order("COUNT(user_groups_users.user_id) " + sortedPaginationRequest.Sort.Direction)
|
||||
|
||||
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &groups)
|
||||
return groups, response, err
|
||||
Order("COUNT(user_groups_users.user_id) " + listRequestOptions.Sort.Direction)
|
||||
}
|
||||
|
||||
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &groups)
|
||||
response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &groups)
|
||||
return groups, response, err
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
||||
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, listRequestOptions utils.ListRequestOptions) ([]model.User, utils.PaginationResponse, error) {
|
||||
var users []model.User
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
@@ -60,7 +60,7 @@ func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPa
|
||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users)
|
||||
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &users)
|
||||
|
||||
return users, pagination, err
|
||||
}
|
||||
@@ -794,11 +794,11 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
|
||||
return user, accessToken, nil
|
||||
}
|
||||
|
||||
func (s *UserService) ListSignupTokens(ctx context.Context, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.SignupToken, utils.PaginationResponse, error) {
|
||||
func (s *UserService) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.SignupToken, utils.PaginationResponse, error) {
|
||||
var tokens []model.SignupToken
|
||||
query := s.db.WithContext(ctx).Model(&model.SignupToken{})
|
||||
|
||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &tokens)
|
||||
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &tokens)
|
||||
return tokens, pagination, err
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
||||
&user,
|
||||
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
|
||||
webauthn.WithExclusions(user.WebAuthnCredentialDescriptors()),
|
||||
webauthn.WithExtensions(map[string]any{"credProps": true}), // Required for Firefox Android to properly save the key in Google password manager
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)
|
||||
@@ -89,6 +90,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
||||
sessionToStore := &model.WebauthnSession{
|
||||
ExpiresAt: datatype.DateTime(session.Expires),
|
||||
Challenge: session.Challenge,
|
||||
CredentialParams: session.CredParams,
|
||||
UserVerification: string(session.UserVerification),
|
||||
}
|
||||
|
||||
@@ -130,9 +132,10 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
|
||||
}
|
||||
|
||||
session := webauthn.SessionData{
|
||||
Challenge: storedSession.Challenge,
|
||||
Expires: storedSession.ExpiresAt.ToTime(),
|
||||
UserID: []byte(userID),
|
||||
Challenge: storedSession.Challenge,
|
||||
Expires: storedSession.ExpiresAt.ToTime(),
|
||||
CredParams: storedSession.CredentialParams,
|
||||
UserID: []byte(userID),
|
||||
}
|
||||
|
||||
var user model.User
|
||||
|
||||
@@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -40,3 +41,14 @@ func (d *JSONDuration) UnmarshalJSON(b []byte) error {
|
||||
return errors.New("invalid duration")
|
||||
}
|
||||
}
|
||||
|
||||
func UnmarshalJSONFromDatabase(data interface{}, value any) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, data)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), data)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
205
backend/internal/utils/list_request_util.go
Normal file
205
backend/internal/utils/list_request_util.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type PaginationResponse struct {
|
||||
TotalPages int64 `json:"totalPages"`
|
||||
TotalItems int64 `json:"totalItems"`
|
||||
CurrentPage int `json:"currentPage"`
|
||||
ItemsPerPage int `json:"itemsPerPage"`
|
||||
}
|
||||
|
||||
type ListRequestOptions struct {
|
||||
Pagination struct {
|
||||
Page int `form:"pagination[page]"`
|
||||
Limit int `form:"pagination[limit]"`
|
||||
} `form:"pagination"`
|
||||
Sort struct {
|
||||
Column string `form:"sort[column]"`
|
||||
Direction string `form:"sort[direction]"`
|
||||
} `form:"sort"`
|
||||
Filters map[string][]any
|
||||
}
|
||||
|
||||
type FieldMeta struct {
|
||||
ColumnName string
|
||||
IsSortable bool
|
||||
IsFilterable bool
|
||||
}
|
||||
|
||||
func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOptions) {
|
||||
if err := ctx.ShouldBindQuery(&listRequestOptions); err != nil {
|
||||
return listRequestOptions
|
||||
}
|
||||
|
||||
listRequestOptions.Filters = parseNestedFilters(ctx)
|
||||
return listRequestOptions
|
||||
}
|
||||
|
||||
func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||
meta := extractModelMetadata(result)
|
||||
|
||||
query = applyFilters(params.Filters, query, meta)
|
||||
query = applySorting(params.Sort.Column, params.Sort.Direction, query, meta)
|
||||
|
||||
return Paginate(params.Pagination.Page, params.Pagination.Limit, query, result)
|
||||
}
|
||||
|
||||
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
} else if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
var totalItems int64
|
||||
if err := query.Count(&totalItems).Error; err != nil {
|
||||
return PaginationResponse{}, err
|
||||
}
|
||||
|
||||
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
||||
if totalItems == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
if int64(page) > totalPages {
|
||||
page = int(totalPages)
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
||||
return PaginationResponse{}, err
|
||||
}
|
||||
|
||||
return PaginationResponse{
|
||||
TotalPages: totalPages,
|
||||
TotalItems: totalItems,
|
||||
CurrentPage: page,
|
||||
ItemsPerPage: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NormalizeSortDirection(direction string) string {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
if d != "asc" && d != "desc" {
|
||||
return "asc"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func IsValidSortDirection(direction string) bool {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
return d == "asc" || d == "desc"
|
||||
}
|
||||
|
||||
// parseNestedFilters handles ?filters[field][0]=val1&filters[field][1]=val2
|
||||
func parseNestedFilters(ctx *gin.Context) map[string][]any {
|
||||
result := make(map[string][]any)
|
||||
query := ctx.Request.URL.Query()
|
||||
|
||||
for key, values := range query {
|
||||
if !strings.HasPrefix(key, "filters[") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Keys can be "filters[field]" or "filters[field][0]"
|
||||
raw := strings.TrimPrefix(key, "filters[")
|
||||
// Take everything up to the first closing bracket
|
||||
if idx := strings.IndexByte(raw, ']'); idx != -1 {
|
||||
field := raw[:idx]
|
||||
for _, v := range values {
|
||||
result[field] = append(result[field], ConvertStringToType(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// applyFilters applies filtering to the GORM query based on the provided filters
|
||||
func applyFilters(filters map[string][]any, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
|
||||
for key, values := range filters {
|
||||
if key == "" || len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldName := CapitalizeFirstLetter(key)
|
||||
fieldMeta, ok := meta[fieldName]
|
||||
if !ok || !fieldMeta.IsFilterable {
|
||||
continue
|
||||
}
|
||||
|
||||
query = query.Where(fieldMeta.ColumnName+" IN ?", values)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// applySorting applies sorting to the GORM query based on the provided column and direction
|
||||
func applySorting(sortColumn string, sortDirection string, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
|
||||
fieldName := CapitalizeFirstLetter(sortColumn)
|
||||
fieldMeta, ok := meta[fieldName]
|
||||
if !ok || !fieldMeta.IsSortable {
|
||||
return query
|
||||
}
|
||||
|
||||
sortDirection = NormalizeSortDirection(sortDirection)
|
||||
|
||||
query = query.Clauses(clause.OrderBy{
|
||||
Columns: []clause.OrderByColumn{
|
||||
{Column: clause.Column{Name: fieldMeta.ColumnName}, Desc: sortDirection == "desc"},
|
||||
},
|
||||
})
|
||||
return query
|
||||
}
|
||||
|
||||
// extractModelMetadata extracts FieldMeta from the model struct using reflection
|
||||
func extractModelMetadata(model interface{}) map[string]FieldMeta {
|
||||
meta := make(map[string]FieldMeta)
|
||||
|
||||
// Unwrap pointers and slices to get the element struct type
|
||||
t := reflect.TypeOf(model)
|
||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
if t == nil {
|
||||
return meta
|
||||
}
|
||||
}
|
||||
|
||||
// recursive parser that merges fields from embedded structs
|
||||
var parseStruct func(reflect.Type)
|
||||
parseStruct = func(st reflect.Type) {
|
||||
for i := 0; i < st.NumField(); i++ {
|
||||
field := st.Field(i)
|
||||
ft := field.Type
|
||||
|
||||
// If the field is an embedded/anonymous struct, recurse into it
|
||||
if field.Anonymous && ft.Kind() == reflect.Struct {
|
||||
parseStruct(ft)
|
||||
continue
|
||||
}
|
||||
|
||||
// Normal field: record metadata
|
||||
name := field.Name
|
||||
meta[name] = FieldMeta{
|
||||
ColumnName: CamelCaseToSnakeCase(name),
|
||||
IsSortable: field.Tag.Get("sortable") == "true",
|
||||
IsFilterable: field.Tag.Get("filterable") == "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseStruct(t)
|
||||
return meta
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type PaginationResponse struct {
|
||||
TotalPages int64 `json:"totalPages"`
|
||||
TotalItems int64 `json:"totalItems"`
|
||||
CurrentPage int `json:"currentPage"`
|
||||
ItemsPerPage int `json:"itemsPerPage"`
|
||||
}
|
||||
|
||||
type SortedPaginationRequest struct {
|
||||
Pagination struct {
|
||||
Page int `form:"pagination[page]"`
|
||||
Limit int `form:"pagination[limit]"`
|
||||
} `form:"pagination"`
|
||||
Sort struct {
|
||||
Column string `form:"sort[column]"`
|
||||
Direction string `form:"sort[direction]"`
|
||||
} `form:"sort"`
|
||||
}
|
||||
|
||||
func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||
pagination := sortedPaginationRequest.Pagination
|
||||
sort := sortedPaginationRequest.Sort
|
||||
|
||||
capitalizedSortColumn := CapitalizeFirstLetter(sort.Column)
|
||||
|
||||
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
|
||||
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
|
||||
|
||||
sort.Direction = NormalizeSortDirection(sort.Direction)
|
||||
|
||||
if sortFieldFound && isSortable {
|
||||
columnName := CamelCaseToSnakeCase(sort.Column)
|
||||
query = query.Clauses(clause.OrderBy{
|
||||
Columns: []clause.OrderByColumn{
|
||||
{Column: clause.Column{Name: columnName}, Desc: sort.Direction == "desc"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return Paginate(pagination.Page, pagination.Limit, query, result)
|
||||
}
|
||||
|
||||
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
} else if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
var totalItems int64
|
||||
if err := query.Count(&totalItems).Error; err != nil {
|
||||
return PaginationResponse{}, err
|
||||
}
|
||||
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
||||
return PaginationResponse{}, err
|
||||
}
|
||||
|
||||
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
||||
if totalItems == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
return PaginationResponse{
|
||||
TotalPages: totalPages,
|
||||
TotalItems: totalItems,
|
||||
CurrentPage: page,
|
||||
ItemsPerPage: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NormalizeSortDirection(direction string) string {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
if d != "asc" && d != "desc" {
|
||||
return "asc"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func IsValidSortDirection(direction string) bool {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
return d == "asc" || d == "desc"
|
||||
}
|
||||
@@ -81,26 +81,21 @@ func CapitalizeFirstLetter(str string) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func CamelCaseToSnakeCase(str string) string {
|
||||
result := strings.Builder{}
|
||||
result.Grow(int(float32(len(str)) * 1.1))
|
||||
for i, r := range str {
|
||||
if unicode.IsUpper(r) && i > 0 {
|
||||
result.WriteByte('_')
|
||||
}
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
return result.String()
|
||||
var (
|
||||
reAcronymBoundary = regexp.MustCompile(`([A-Z]+)([A-Z][a-z])`) // ABCd -> AB_Cd
|
||||
reLowerToUpper = regexp.MustCompile(`([a-z0-9])([A-Z])`) // aB -> a_B
|
||||
)
|
||||
|
||||
func CamelCaseToSnakeCase(s string) string {
|
||||
s = reAcronymBoundary.ReplaceAllString(s, "${1}_${2}")
|
||||
s = reLowerToUpper.ReplaceAllString(s, "${1}_${2}")
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
var camelCaseToScreamingSnakeCaseRe = regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||
|
||||
func CamelCaseToScreamingSnakeCase(s string) string {
|
||||
// Insert underscores before uppercase letters (except the first one)
|
||||
snake := camelCaseToScreamingSnakeCaseRe.ReplaceAllString(s, `${1}_${2}`)
|
||||
|
||||
// Convert to uppercase
|
||||
return strings.ToUpper(snake)
|
||||
s = reAcronymBoundary.ReplaceAllString(s, "${1}_${2}")
|
||||
s = reLowerToUpper.ReplaceAllString(s, "${1}_${2}")
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
// GetFirstCharacter returns the first non-whitespace character of the string, correctly handling Unicode
|
||||
|
||||
@@ -86,9 +86,9 @@ func TestCamelCaseToSnakeCase(t *testing.T) {
|
||||
{"simple camelCase", "camelCase", "camel_case"},
|
||||
{"PascalCase", "PascalCase", "pascal_case"},
|
||||
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "multiple_words_in_camel_case"},
|
||||
{"consecutive uppercase", "HTTPRequest", "h_t_t_p_request"},
|
||||
{"consecutive uppercase", "HTTPRequest", "http_request"},
|
||||
{"single lowercase word", "word", "word"},
|
||||
{"single uppercase word", "WORD", "w_o_r_d"},
|
||||
{"single uppercase word", "WORD", "word"},
|
||||
{"with numbers", "camel123Case", "camel123_case"},
|
||||
{"with numbers in middle", "model2Name", "model2_name"},
|
||||
{"mixed case", "iPhone6sPlus", "i_phone6s_plus"},
|
||||
@@ -104,6 +104,34 @@ func TestCamelCaseToSnakeCase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCamelCaseToScreamingSnakeCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"empty string", "", ""},
|
||||
{"simple camelCase", "camelCase", "CAMEL_CASE"},
|
||||
{"PascalCase", "PascalCase", "PASCAL_CASE"},
|
||||
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "MULTIPLE_WORDS_IN_CAMEL_CASE"},
|
||||
{"consecutive uppercase", "HTTPRequest", "HTTP_REQUEST"},
|
||||
{"single lowercase word", "word", "WORD"},
|
||||
{"single uppercase word", "WORD", "WORD"},
|
||||
{"with numbers", "camel123Case", "CAMEL123_CASE"},
|
||||
{"with numbers in middle", "model2Name", "MODEL2_NAME"},
|
||||
{"mixed case", "iPhone6sPlus", "I_PHONE6S_PLUS"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := CamelCaseToScreamingSnakeCase(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CamelCaseToScreamingSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFirstCharacter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
35
backend/internal/utils/type_util.go
Normal file
35
backend/internal/utils/type_util.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ConvertStringToType attempts to convert a string to bool, int, or float.
|
||||
func ConvertStringToType(value string) any {
|
||||
v := strings.TrimSpace(value)
|
||||
if v == "" {
|
||||
return v
|
||||
}
|
||||
|
||||
// Try bool
|
||||
if v == "true" {
|
||||
return true
|
||||
}
|
||||
if v == "false" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try int
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
// Try float
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
|
||||
// Default: string
|
||||
return v
|
||||
}
|
||||
37
backend/internal/utils/type_util_test.go
Normal file
37
backend/internal/utils/type_util_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvertStringToType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected any
|
||||
}{
|
||||
{"true", true},
|
||||
{"false", false},
|
||||
{" true ", true},
|
||||
{" false ", false},
|
||||
{"42", 42},
|
||||
{" 42 ", 42},
|
||||
{"3.14", 3.14},
|
||||
{" 3.14 ", 3.14},
|
||||
{"hello", "hello"},
|
||||
{" hello ", "hello"},
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := ConvertStringToType(tt.input)
|
||||
if result != tt.expected {
|
||||
if f, ok := tt.expected.(float64); ok {
|
||||
if rf, ok := result.(float64); ok && rf == f {
|
||||
continue
|
||||
}
|
||||
}
|
||||
t.Errorf("ConvertStringToType(%q) = %#v (type %T), want %#v (type %T)", tt.input, result, result, tt.expected, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN dark_image_type;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN dark_image_type TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE webauthn_sessions DROP COLUMN credential_params;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE webauthn_sessions ADD COLUMN credential_params JSONB NOT NULL DEFAULT '[]';
|
||||
@@ -0,0 +1,5 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
ALTER TABLE oidc_clients DROP COLUMN dark_image_type;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,5 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
ALTER TABLE oidc_clients ADD COLUMN dark_image_type TEXT;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,5 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
ALTER TABLE webauthn_sessions DROP COLUMN credential_params;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,5 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
ALTER TABLE webauthn_sessions ADD COLUMN credential_params TEXT NOT NULL DEFAULT '[]';
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -10,16 +10,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.1.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-email/preview-server": "4.2.8",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.1",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"react-email": "4.2.8",
|
||||
"tsx": "^4.0.0"
|
||||
"tsx": "^4.20.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Vložte přímou URL adresu obrázku (svg, png, webp). Ikony najdete na <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> nebo <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Neplatná adresa URL",
|
||||
"require_user_email": "Vyžadovat e-mailovou adresu",
|
||||
"require_user_email_description": "Vyžaduje, aby uživatelé měli e-mailovou adresu. Pokud je tato možnost deaktivována, uživatelé bez e-mailové adresy nebudou moci používat funkce, které e-mailovou adresu vyžadují."
|
||||
"require_user_email_description": "Vyžaduje, aby uživatelé měli e-mailovou adresu. Pokud je tato možnost deaktivována, uživatelé bez e-mailové adresy nebudou moci používat funkce, které e-mailovou adresu vyžadují.",
|
||||
"view": "Zobrazit",
|
||||
"toggle_columns": "Přepínat sloupce",
|
||||
"locale": "Místní nastavení",
|
||||
"ldap_id": "LDAP ID",
|
||||
"reauthentication": "Opětovné ověření",
|
||||
"clear_filters": "Vymazat filtry"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Indsæt en direkte billed-URL (svg, png, webp). Find ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Ugyldig URL",
|
||||
"require_user_email": "Kræver e-mailadresse",
|
||||
"require_user_email_description": "Kræver, at brugerne har en e-mailadresse. Hvis denne funktion er deaktiveret, kan brugere uden en e-mailadresse ikke bruge funktioner, der kræver en e-mailadresse."
|
||||
"require_user_email_description": "Kræver, at brugerne har en e-mailadresse. Hvis denne funktion er deaktiveret, kan brugere uden en e-mailadresse ikke bruge funktioner, der kræver en e-mailadresse.",
|
||||
"view": "Vis",
|
||||
"toggle_columns": "Skift kolonner",
|
||||
"locale": "Lokale",
|
||||
"ldap_id": "LDAP-id",
|
||||
"reauthentication": "Genbekræftelse",
|
||||
"clear_filters": "Ryd filtre"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Füge eine direkte Bild-URL ein (svg, png, webp). Finde Symbole bei <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> oder <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Ungültige URL",
|
||||
"require_user_email": "E-Mail-Adresse erforderlich",
|
||||
"require_user_email_description": "Benutzer müssen eine E-Mail-Adresse haben. Wenn das deaktiviert ist, können Leute ohne E-Mail-Adresse die Funktionen, die eine E-Mail-Adresse brauchen, nicht nutzen."
|
||||
"require_user_email_description": "Benutzer müssen eine E-Mail-Adresse haben. Wenn das deaktiviert ist, können Leute ohne E-Mail-Adresse die Funktionen, die eine E-Mail-Adresse brauchen, nicht nutzen.",
|
||||
"view": "Ansicht",
|
||||
"toggle_columns": "Spalten umschalten",
|
||||
"locale": "Ort",
|
||||
"ldap_id": "LDAP-ID",
|
||||
"reauthentication": "Erneute Authentifizierung",
|
||||
"clear_filters": "Filter löschen"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> or <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Invalid URL",
|
||||
"require_user_email": "Require Email Address",
|
||||
"require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address."
|
||||
"require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address.",
|
||||
"view": "View",
|
||||
"toggle_columns": "Toggle columns",
|
||||
"locale": "Locale",
|
||||
"ldap_id" : "LDAP ID",
|
||||
"reauthentication": "Re-authentication",
|
||||
"clear_filters" : "Clear Filters"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Pega una URL de imagen directa (svg, png, webp). Encuentra iconos en <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "URL no válida",
|
||||
"require_user_email": "Requerir dirección de correo electrónico",
|
||||
"require_user_email_description": "Requiere que los usuarios tengan una dirección de correo electrónico. Si se desactiva, los usuarios que no tengan una dirección de correo electrónico no podrán utilizar las funciones que la requieran."
|
||||
"require_user_email_description": "Requiere que los usuarios tengan una dirección de correo electrónico. Si se desactiva, los usuarios que no tengan una dirección de correo electrónico no podrán utilizar las funciones que la requieran.",
|
||||
"view": "Ver",
|
||||
"toggle_columns": "Alternar columnas",
|
||||
"locale": "Configuración regional",
|
||||
"ldap_id": "Identificador LDAP",
|
||||
"reauthentication": "Reautenticación",
|
||||
"clear_filters": "Borrar filtros"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Colle une URL d'image directe (svg, png, webp). Trouve des icônes sur <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "URL pas valide",
|
||||
"require_user_email": "Besoin d'une adresse e-mail",
|
||||
"require_user_email_description": "Les utilisateurs doivent avoir une adresse e-mail. Si cette option est désactivée, ceux qui n'ont pas d'adresse e-mail ne pourront pas utiliser les fonctionnalités qui en ont besoin."
|
||||
"require_user_email_description": "Les utilisateurs doivent avoir une adresse e-mail. Si cette option est désactivée, ceux qui n'ont pas d'adresse e-mail ne pourront pas utiliser les fonctionnalités qui en ont besoin.",
|
||||
"view": "Voir",
|
||||
"toggle_columns": "Basculer les colonnes",
|
||||
"locale": "Paramètres régionaux",
|
||||
"ldap_id": "ID LDAP",
|
||||
"reauthentication": "Réauthentification",
|
||||
"clear_filters": "Effacer les filtres"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Incolla l'URL diretto dell'immagine (svg, png, webp). Trova le icone su <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "URL non valido",
|
||||
"require_user_email": "Richiesta indirizzo e-mail",
|
||||
"require_user_email_description": "Chiede agli utenti di avere un indirizzo email. Se disattivato, chi non ha un indirizzo email non potrà usare le funzioni che lo richiedono."
|
||||
"require_user_email_description": "Chiede agli utenti di avere un indirizzo email. Se disattivato, chi non ha un indirizzo email non potrà usare le funzioni che lo richiedono.",
|
||||
"view": "Visualizza",
|
||||
"toggle_columns": "Attiva/disattiva colonne",
|
||||
"locale": "Locale",
|
||||
"ldap_id": "ID LDAP",
|
||||
"reauthentication": "Riautenticazione",
|
||||
"clear_filters": "Cancella filtri"
|
||||
}
|
||||
|
||||
465
frontend/messages/ja.json
Normal file
465
frontend/messages/ja.json
Normal file
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "マイアカウント",
|
||||
"logout": "ログアウト",
|
||||
"confirm": "確認",
|
||||
"docs": "ドキュメント",
|
||||
"key": "キー",
|
||||
"value": "値",
|
||||
"remove_custom_claim": "カスタムクレームを削除",
|
||||
"add_custom_claim": "カスタムクレームを追加",
|
||||
"add_another": "さらに追加",
|
||||
"select_a_date": "日付を選択",
|
||||
"select_file": "ファイルを選択",
|
||||
"profile_picture": "プロフィール画像",
|
||||
"profile_picture_is_managed_by_ldap_server": "プロフィール画像はLDAPサーバーによって管理されており、ここでは変更できません。",
|
||||
"click_profile_picture_to_upload_custom": "プロフィール画像をクリックして、ファイルからカスタム画像をアップロードします。",
|
||||
"image_should_be_in_format": "画像はPNGまたはJPEG形式である必要があります。",
|
||||
"items_per_page": "ページあたりの表示件数",
|
||||
"no_items_found": "項目が見つかりません",
|
||||
"select_items": "項目を選択…",
|
||||
"search": "検索…",
|
||||
"expand_card": "カードを展開",
|
||||
"copied": "コピーしました",
|
||||
"click_to_copy": "クリックしてコピー",
|
||||
"something_went_wrong": "エラーが発生しました",
|
||||
"go_back_to_home": "ホームへ戻る",
|
||||
"alternative_sign_in_methods": "別のサインイン方法",
|
||||
"login_background": "ログインの背景",
|
||||
"logo": "ロゴ",
|
||||
"login_code": "ログインコード",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "ユーザーがパスキーなしで一度だけサインインできるログインコードを作成する。",
|
||||
"one_hour": "1 時間",
|
||||
"twelve_hours": "12 時間",
|
||||
"one_day": "1 日",
|
||||
"one_week": "1 週間",
|
||||
"one_month": "1 ヶ月",
|
||||
"expiration": "有効期限",
|
||||
"generate_code": "コードの生成",
|
||||
"name": "名前",
|
||||
"browser_unsupported": "ブラウザ未対応",
|
||||
"this_browser_does_not_support_passkeys": "このブラウザはパスキーをサポートしていません。別のサインイン方法を使用してください。",
|
||||
"an_unknown_error_occurred": "不明なエラーが発生しました",
|
||||
"authentication_process_was_aborted": "認証プロセスが中止されました",
|
||||
"error_occurred_with_authenticator": "認証ツールでエラーが発生しました",
|
||||
"authenticator_does_not_support_discoverable_credentials": "認証ツールは discoverable credential をサポートしていません",
|
||||
"authenticator_does_not_support_resident_keys": "認証ツールは resident key をサポートしていません",
|
||||
"passkey_was_previously_registered": "このパスキーは既に登録されています",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "認証ツールは要求されたアルゴリズムのいずれをもサポートしていません",
|
||||
"authenticator_timed_out": "認証ツールがタイムアウトしました",
|
||||
"critical_error_occurred_contact_administrator": "重大なエラーが発生しました。管理者にお問い合わせください。",
|
||||
"sign_in_to": "{name} にサインイン",
|
||||
"client_not_found": "クライアントが見つかりません",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> は以下の情報にアクセスしようとしています:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "{appName} アカウントで <b>{client}</b> にサインインしますか?",
|
||||
"email": "メール",
|
||||
"view_your_email_address": "メールアドレスを表示",
|
||||
"profile": "プロフィール",
|
||||
"view_your_profile_information": "プロフィール情報を表示",
|
||||
"groups": "グループ",
|
||||
"view_the_groups_you_are_a_member_of": "あなたがメンバーであるグループを表示",
|
||||
"cancel": "キャンセル",
|
||||
"sign_in": "サインイン",
|
||||
"try_again": "リトライ",
|
||||
"client_logo": "クライアントロゴ",
|
||||
"sign_out": "サインアウト",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "<b>{username}</b> アカウントで {appName} からサインアウトしますか?",
|
||||
"sign_in_to_appname": "{appName} にサインイン",
|
||||
"please_try_to_sign_in_again": "もう一度サインインしてください。",
|
||||
"authenticate_with_passkey_to_access_account": "アカウントにアクセスするには、パスキーで認証してください。",
|
||||
"authenticate": "認証",
|
||||
"please_try_again": "再度お試しください。",
|
||||
"continue": "続行",
|
||||
"alternative_sign_in": "別のサインイン",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "パスキーにアクセスできない場合は、以下のいずれかの方法でサインインできます。",
|
||||
"use_your_passkey_instead": "代わりにパスキーを使用しますか?",
|
||||
"email_login": "メールでログイン",
|
||||
"enter_a_login_code_to_sign_in": "ログインコードを入力してサインインしてください。",
|
||||
"sign_in_with_login_code": "ログインコードでサインイン",
|
||||
"request_a_login_code_via_email": "メールでログインコードをリクエストする。",
|
||||
"go_back": "戻る",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "システムに存在する場合、指定されたメールアドレスにメールが送信されました。",
|
||||
"enter_code": "コードを入力",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "ログインコードのメールを受信するには、メールアドレスを入力してください。",
|
||||
"your_email": "あなたのメール",
|
||||
"submit": "送信",
|
||||
"enter_the_code_you_received_to_sign_in": "サインインするために受け取ったコードを入力してください。",
|
||||
"code": "コード",
|
||||
"invalid_redirect_url": "無効な redirect URL",
|
||||
"audit_log": "監査ログ",
|
||||
"users": "ユーザー",
|
||||
"user_groups": "ユーザーグループ",
|
||||
"oidc_clients": "OIDCクライアント",
|
||||
"api_keys": "API キー",
|
||||
"application_configuration": "アプリケーション設定",
|
||||
"settings": "設定",
|
||||
"update_pocket_id": "Pocket ID を更新",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "過去3ヶ月間のアカウントアクティビティを確認する。",
|
||||
"time": "時間",
|
||||
"event": "イベント",
|
||||
"approximate_location": "おおよその場所",
|
||||
"ip_address": "IPアドレス",
|
||||
"device": "デバイス",
|
||||
"client": "クライアント",
|
||||
"unknown": "不明",
|
||||
"account_details_updated_successfully": "アカウントの詳細が正常に更新されました",
|
||||
"profile_picture_updated_successfully": "プロフィール画像が正常に更新されました。反映まで数分かかる場合があります。",
|
||||
"account_settings": "アカウント設定",
|
||||
"passkey_missing": "パスキーが見つかりません",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "アカウントへのアクセスを失わないように、パスキーを追加してください。",
|
||||
"single_passkey_configured": "Single Passkey Configured",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "アカウントへのアクセスを失わないように、複数のパスキーを追加することをお勧めします。",
|
||||
"account_details": "アカウントの詳細",
|
||||
"passkeys": "パスキー",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "認証に使用するパスキーを管理します。",
|
||||
"add_passkey": "パスキーを追加",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "パスキーなしで別のデバイスからサインインするためのワンタイムログインコードを作成します。",
|
||||
"create": "作成",
|
||||
"first_name": "名",
|
||||
"last_name": "姓",
|
||||
"username": "ユーザー名",
|
||||
"save": "保存",
|
||||
"username_can_only_contain": "ユーザー名には小文字、数字、アンダー スコア、ドット、ハイフン、および '@' 記号のみを含めることができます",
|
||||
"username_must_start_with": "ユーザー名は英数字で始まる必要があります",
|
||||
"username_must_end_with": "ユーザー名は英数字で終わる必要があります",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "以下のコードを使用してサインインしてください。コードは15分後に失効します。",
|
||||
"or_visit": "or visit",
|
||||
"added_on": "追加日",
|
||||
"rename": "名前を変更",
|
||||
"delete": "削除",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "このパスキーを削除してもよろしいですか?",
|
||||
"passkey_deleted_successfully": "パスキーが正常に削除されました",
|
||||
"delete_passkey_name": "{passkeyName} を削除",
|
||||
"passkey_name_updated_successfully": "パスキー名が正常に更新されました",
|
||||
"name_passkey": "パスキー名",
|
||||
"name_your_passkey_to_easily_identify_it_later": "パスキーを後で簡単に識別するように名前を付けます。",
|
||||
"create_api_key": "API キーを作成",
|
||||
"add_a_new_api_key_for_programmatic_access": "<link href='https://pocket-id.org/docs/api'>Pocket ID API</link> へのプログラムによるアクセス用に新しいAPI キーを追加します。",
|
||||
"add_api_key": "API キーを追加",
|
||||
"manage_api_keys": "API キーの管理",
|
||||
"api_key_created": "APIキー が作成されました",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "セキュリティ上の理由から、このキーは一度だけ表示されます。安全に保管してください。",
|
||||
"description": "説明",
|
||||
"api_key": "API キー",
|
||||
"close": "閉じる",
|
||||
"name_to_identify_this_api_key": "この API キーを識別するための名前。",
|
||||
"expires_at": "有効期限",
|
||||
"when_this_api_key_will_expire": "このAPI キーが期限切れになる日付。",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "このキーの目的を特定するのに役立つ任意の説明。",
|
||||
"expiration_date_must_be_in_the_future": "有効期限は未来の日付にする必要があります",
|
||||
"revoke_api_key": "API キーの削除",
|
||||
"never": "なし",
|
||||
"revoke": "削除",
|
||||
"api_key_revoked_successfully": "API キーが正常に削除されました",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "APIキー \"{apiKeyName}\" を本当に削除しますか?このAPI キーを使用しているすべての連携が停止します。",
|
||||
"last_used": "最終使用日",
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "画像が正常に更新されました",
|
||||
"general": "一般",
|
||||
"configure_smtp_to_send_emails": "新しいデバイスや場所からのログインが検出された際にユーザーに警告するメール通知を有効にします。",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP サーバーからユーザーとグループを同期するように LDAP 設定を構成します。",
|
||||
"images": "画像",
|
||||
"update": "更新",
|
||||
"email_configuration_updated_successfully": "メール設定が正常に更新されました",
|
||||
"save_changes_question": "変更を保存しますか?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "テストメールを送信する前に変更を保存する必要があります。今すぐ保存しますか?",
|
||||
"save_and_send": "保存して送信",
|
||||
"test_email_sent_successfully": "テストメールがあなたのメールアドレスに正常に送信されました。",
|
||||
"failed_to_send_test_email": "テストメールの送信に失敗しました。詳細についてはサーバーのログを確認してください。",
|
||||
"smtp_configuration": "SMTP 設定",
|
||||
"smtp_host": "SMTP ホスト",
|
||||
"smtp_port": "SMTP ポート",
|
||||
"smtp_user": "SMTP ユーザー",
|
||||
"smtp_password": "SMTP パスワード",
|
||||
"smtp_from": "SMTP 送信元",
|
||||
"smtp_tls_option": "SMTP TLS オプション",
|
||||
"email_tls_option": "メール TLS オプション",
|
||||
"skip_certificate_verification": "証明書検証をスキップ",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "これは自己署名証明書の場合に有用です。",
|
||||
"enabled_emails": "有効なメール",
|
||||
"email_login_notification": "メールログイン通知",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "ユーザーが新しいデバイスからログインした際にメールを送信する。",
|
||||
"emai_login_code_requested_by_user": "ユーザーがリクエストしたメールログインコード",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "ユーザーがメールに送信されたログインコードをリクエストすることでパスキーをバイパスできるようにします。これによりセキュリティが大幅に低下し、ユーザーのメールにアクセスできる者なら誰でもログイン可能になります。",
|
||||
"email_login_code_from_admin": "管理者からのメールログインコード",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "管理者がメールでログインコードをユーザーに送信することを許可します。",
|
||||
"send_test_email": "テストメールを送信",
|
||||
"application_configuration_updated_successfully": "アプリケーション設定が正常に更新されました",
|
||||
"application_name": "アプリケーション名",
|
||||
"session_duration": "セッション期間",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "ユーザーが再度ログインする必要があるまでのセッションの継続時間。(分単位)",
|
||||
"enable_self_account_editing": "自身のアカウント編集を有効にする",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "ユーザーが自身のアカウントの詳細を編集できるかどうか。",
|
||||
"emails_verified": "メールアドレス確認済み",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "ユーザのEメールをOIDCクライアントで検証済みとしてマークするかどうか。",
|
||||
"ldap_configuration_updated_successfully": "LDAP 設定が正常に更新されました",
|
||||
"ldap_disabled_successfully": "LDAPは正常に無効化されました",
|
||||
"ldap_sync_finished": "LDAP同期が完了しました",
|
||||
"client_configuration": "クライアントの設定",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "ユーザー検索フィルター",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "ユーザーの検索、同期に使用する検索フィルター。",
|
||||
"groups_search_filter": "グループ検索フィルター",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "グループの検索、同期に使用する検索フィルター。",
|
||||
"attribute_mapping": "属性マッピング",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"the_value_of_this_attribute_should_never_change": "この属性の値は決して変更されてはなりません。",
|
||||
"username_attribute": "ユーザー名属性",
|
||||
"user_mail_attribute": "ユーザーメール属性",
|
||||
"user_first_name_attribute": "ユーザーの名属性",
|
||||
"user_last_name_attribute": "ユーザーの姓属性",
|
||||
"user_profile_picture_attribute": "ユーザープロフィール画像属性",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "この属性の値は、URL、バイナリ、またはBase64エンコードされた画像のいずれかです。",
|
||||
"group_members_attribute": "グループメンバー属性",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "グループのメンバーをクエリする際に使用する属性。",
|
||||
"group_unique_identifier_attribute": "グループ固有識別子属性",
|
||||
"group_rdn_attribute": "Group RDN Attribute (in DN)",
|
||||
"admin_group_name": "管理者グループ名",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "このグループのメンバーはPocket IDで管理者権限を持っています。",
|
||||
"disable": "無効",
|
||||
"sync_now": "今すぐ同期",
|
||||
"enable": "有効",
|
||||
"user_created_successfully": "ユーザーは正常に作成されました",
|
||||
"create_user": "ユーザーを作成",
|
||||
"add_a_new_user_to_appname": "{appName} に新しいユーザーを追加",
|
||||
"add_user": "ユーザーを追加",
|
||||
"manage_users": "ユーザー管理",
|
||||
"admin_privileges": "管理者権限",
|
||||
"admins_have_full_access_to_the_admin_panel": "管理者は管理パネルへのフルアクセス権限を持っています。",
|
||||
"delete_firstname_lastname": "削除 {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "このユーザーを削除してもよろしいですか?",
|
||||
"user_deleted_successfully": "ユーザーの削除が正常に完了しました",
|
||||
"role": "ロール",
|
||||
"source": "Source",
|
||||
"admin": "管理者",
|
||||
"user": "ユーザー",
|
||||
"local": "ローカル",
|
||||
"toggle_menu": "Toggle menu",
|
||||
"edit": "編集",
|
||||
"user_groups_updated_successfully": "ユーザーグループが正常に更新されました",
|
||||
"user_updated_successfully": "ユーザーは正常に更新されました",
|
||||
"custom_claims_updated_successfully": "カスタムクレームが正常に更新されました",
|
||||
"back": "Back",
|
||||
"user_details_firstname_lastname": "ユーザーの詳細 {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "このユーザーが所属するグループを管理します。",
|
||||
"custom_claims": "カスタムクレーム",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。",
|
||||
"user_group_created_successfully": "ユーザーグループが正常に作成されました",
|
||||
"create_user_group": "ユーザーグループを作成",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "ユーザーに割り当て可能な新しいグループを作成します。",
|
||||
"add_group": "グループを追加",
|
||||
"manage_user_groups": "ユーザーグループの管理",
|
||||
"friendly_name": "Friendly Name",
|
||||
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
|
||||
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||
"delete_name": "{name} を削除",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "このユーザーグループを削除しますか?",
|
||||
"user_group_deleted_successfully": "ユーザーグループが正常に削除されました",
|
||||
"user_count": "ユーザー数",
|
||||
"user_group_updated_successfully": "ユーザーグループが正常に更新されました",
|
||||
"users_updated_successfully": "ユーザーは正常に更新されました",
|
||||
"user_group_details_name": "ユーザーグループの詳細 {name}",
|
||||
"assign_users_to_this_group": "このグループにユーザーを割り当てます。",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。競合が発生した場合、ユーザーに定義されたカスタムクレームが優先されます。",
|
||||
"oidc_client_created_successfully": "OIDC クライアントが正常に作成されました",
|
||||
"create_oidc_client": "OIDC クライアントの作成",
|
||||
"add_a_new_oidc_client_to_appname": "{appName} に新しい OIDC クライアントを追加します。",
|
||||
"add_oidc_client": "OIDC クライアントを追加",
|
||||
"manage_oidc_clients": "OIDC クライアントの管理",
|
||||
"one_time_link": "ワンタイムリンク",
|
||||
"use_this_link_to_sign_in_once": "このリンクを使用してサインインしてください。これは、まだパスキーを追加していないユーザーや、パスキーを紛失したユーザーに必要な手順です。",
|
||||
"add": "追加",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"requires_reauthentication": "再認証が必要",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in",
|
||||
"name_logo": "{name} ロゴ",
|
||||
"change_logo": "ロゴの変更",
|
||||
"upload_logo": "ロゴをアップロード",
|
||||
"remove_logo": "ロゴを削除",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "このOIDCクライアントを削除してもよろしいですか?",
|
||||
"oidc_client_deleted_successfully": "OIDC クライアントが正常に削除されました",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "有効",
|
||||
"disabled": "無効",
|
||||
"oidc_client_updated_successfully": "OIDC クライアントが正常に更新されました",
|
||||
"create_new_client_secret": "新しいクライアントシークレットを作成する",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "新しいクライアントシークレットを作成してもよろしいですか?古いシークレットは無効化されます。",
|
||||
"generate": "Generate",
|
||||
"new_client_secret_created_successfully": "新しいクライアントシークレットが正常に作成されました",
|
||||
"allowed_user_groups_updated_successfully": "許可されたユーザーグループが正常に更新されました",
|
||||
"oidc_client_name": "OIDC クライアント {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "クライアントシークレット",
|
||||
"show_more_details": "詳細を表示",
|
||||
"allowed_user_groups": "許可されたユーザーグループ",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "ライトモードのロゴ",
|
||||
"dark_mode_logo": "ダークモードのロゴ",
|
||||
"background_image": "背景画像",
|
||||
"language": "言語",
|
||||
"reset_profile_picture_question": "プロフィール画像をリセットしますか?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "アップロードされた画像を削除し、プロフィール画像をデフォルトにリセットします。続行しますか?",
|
||||
"reset": "リセット",
|
||||
"reset_to_default": "デフォルトに戻す",
|
||||
"profile_picture_has_been_reset": "プロフィール画像がリセットされました。更新には数分かかる場合があります。",
|
||||
"select_the_language_you_want_to_use": "使用する言語を選択してください。一部のテキストは自動翻訳により、不正確な場合がありますのでご注意ください。",
|
||||
"contribute_to_translation": "問題が見つかった場合は、 <link href='https://crowdin.com/project/pocket-id'>Crowdin</link> で翻訳に貢献してください。",
|
||||
"personal": "Personal",
|
||||
"global": "Global",
|
||||
"all_users": "すべてのユーザー",
|
||||
"all_events": "すべてのイベント",
|
||||
"all_clients": "すべてのクライアント",
|
||||
"all_locations": "すべての場所",
|
||||
"global_audit_log": "グローバル監査ログ",
|
||||
"see_all_account_activities_from_the_last_3_months": "過去3ヶ月間のすべてのユーザーアクティビティを表示します。",
|
||||
"token_sign_in": "トークンサインイン",
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "アニメーションの無効化",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "アカウントの無効化",
|
||||
"disabled_users_cannot_log_in_or_use_services": "無効化されたユーザーはログインやサービスを利用できません。",
|
||||
"user_disabled_successfully": "ユーザーが正常に無効化されました。",
|
||||
"user_enabled_successfully": "ユーザーが正常に有効化されました。",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "無効化 {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "LDAPから無効なユーザーを保持します。",
|
||||
"ldap_soft_delete_users_description": "有効にすると、LDAPから削除されたユーザーはシステムから削除されるのではなく、無効化されます。",
|
||||
"login_code_email_success": "ログインコードがユーザーに送信されました。",
|
||||
"send_email": "メールを送信",
|
||||
"show_code": "コードを表示",
|
||||
"callback_url_description": "クライアントが提供するURL。空白のままにすると自動的に追加されます。ワイルドカード(*)はサポートされていますが、セキュリティを向上させるためには避けてください。",
|
||||
"logout_callback_url_description": "クライアントがログアウト用に提供するURL。ワイルドカード(*)はサポートされていますが、セキュリティを向上させるためには避けてください。",
|
||||
"api_key_expiration": "API キーの有効期限",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API キーの有効期限が近づいたら、ユーザーにメールを送信します。",
|
||||
"authorize_device": "デバイスの認証",
|
||||
"the_device_has_been_authorized": "デバイスは認証されました。",
|
||||
"enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。",
|
||||
"authorize": "Authorize",
|
||||
"federated_client_credentials": "連携クライアントの資格情報",
|
||||
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
|
||||
"add_federated_client_credential": "Add Federated Client Credential",
|
||||
"add_another_federated_client_credential": "Add another federated client credential",
|
||||
"oidc_allowed_group_count": "許可されたグループ数",
|
||||
"unrestricted": "制限なし",
|
||||
"show_advanced_options": "詳細設定を表示",
|
||||
"hide_advanced_options": "詳細設定を隠す",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "コピー",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "すべてコピー",
|
||||
"preview": "プレビュー",
|
||||
"preview_for_user": "{name} のプレビュー",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "表示",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "ユーザーを選択",
|
||||
"error": "エラー",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "アクセントカラーを選択して、Pocket IDの外観をカスタマイズしてください。",
|
||||
"accent_color": "アクセントカラー",
|
||||
"custom_accent_color": "カスタムアクセントカラー",
|
||||
"custom_accent_color_description": "有効な CSS カラーフォーマット (例: hex, rgb, hsl) を使用してカスタムカラーを入力します。",
|
||||
"color_value": "Color Value",
|
||||
"apply": "適用",
|
||||
"signup_token": "サインアップトークン",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "新規ユーザー登録を許可するためのサインアップトークンを作成する。",
|
||||
"usage_limit": "使用回数制限",
|
||||
"number_of_times_token_can_be_used": "サインアップトークンが使用できる回数。",
|
||||
"expires": "Expires",
|
||||
"signup": "サインアップ",
|
||||
"user_creation": "ユーザー作成",
|
||||
"configure_user_creation": "新しいユーザーのサインアップ方法やデフォルトの権限など、ユーザー作成設定を管理します。",
|
||||
"user_creation_groups_description": "新しいユーザーにこれらのグループを自動的に割り当てます。",
|
||||
"user_creation_claims_description": "新しいユーザーにこれらのカスタムクレームを自動的に割り当てます。",
|
||||
"user_creation_updated_successfully": "ユーザー作成設定が正常に更新されました。",
|
||||
"signup_disabled_description": "ユーザーのサインアップは無効化されています。管理者のみが新しいユーザーアカウントを作成できます。",
|
||||
"signup_requires_valid_token": "アカウントを作成するには有効なサインアップトークンが必要です",
|
||||
"validating_signup_token": "サインアップトークンを検証中",
|
||||
"go_to_login": "ログインへ移動",
|
||||
"signup_to_appname": "{appName} にサインアップ",
|
||||
"create_your_account_to_get_started": "アカウントを作成して始めましょう。",
|
||||
"initial_account_creation_description": "ご利用を開始するにはアカウントを作成してください。パスキーの設定は後で可能です。",
|
||||
"setup_your_passkey": "パスキーを設定",
|
||||
"create_a_passkey_to_securely_access_your_account": "安全にアカウントにアクセスするためのパスキーを作成します。これがサインインの主な方法になります。",
|
||||
"skip_for_now": "あとで",
|
||||
"account_created": "アカウントが作成されました",
|
||||
"enable_user_signups": "ユーザーサインアップを有効にする",
|
||||
"enable_user_signups_description": "Pocket IDでユーザーが新規アカウントを登録する方法を決定します。",
|
||||
"user_signups_are_disabled": "ユーザーのサインアップは現在無効化されています",
|
||||
"create_signup_token": "サインアップトークンを作成",
|
||||
"view_active_signup_tokens": "有効なサインアップトークンを表示",
|
||||
"manage_signup_tokens": "サインアップトークンの管理",
|
||||
"view_and_manage_active_signup_tokens": "有効なサインアップトークンの表示と管理。",
|
||||
"signup_token_deleted_successfully": "サインアップトークンが正常に削除されました。",
|
||||
"expired": "Expired",
|
||||
"used_up": "Used Up",
|
||||
"active": "Active",
|
||||
"usage": "Usage",
|
||||
"created": "Created",
|
||||
"token": "トークン",
|
||||
"loading": "読み込み中",
|
||||
"delete_signup_token": "サインアップトークンを削除",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "このサインアップトークンを削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"signup_with_token": "トークンでサインアップ",
|
||||
"signup_with_token_description": "ユーザーは管理者が作成した有効なサインアップトークンを使用してのみサインアップできます。",
|
||||
"signup_open": "サインアップを開く",
|
||||
"signup_open_description": "誰でも制限なしに新しいアカウントを作成できます。",
|
||||
"of": "of",
|
||||
"skip_passkey_setup": "パスキーの設定をスキップ",
|
||||
"skip_passkey_setup_description": "パスキーの設定を強く推奨します。設定していない場合、セッションが切れた時点でアカウントにアクセスできなくなります。",
|
||||
"my_apps": "マイアプリ",
|
||||
"no_apps_available": "利用可能なアプリはありません",
|
||||
"contact_your_administrator_for_app_access": "アプリケーションにアクセスするには、管理者に問い合わせてください。",
|
||||
"launch": "起動",
|
||||
"client_launch_url": "Client Launch URL",
|
||||
"client_launch_url_description": "ユーザーが「マイアプリ」ページからアプリを起動した際に開かれるURL。",
|
||||
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
|
||||
"revoke_access": "アクセスを取り消す",
|
||||
"revoke_access_description": "<b>{clientName}</b>へのアクセスを取り消します。 <b>{clientName}</b> はあなたのアカウント情報にアクセスできなくなります。",
|
||||
"revoke_access_successful": "{clientName} へのアクセスは正常に取り消されました。",
|
||||
"last_signed_in_ago": "Last signed in {time} ago",
|
||||
"invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens",
|
||||
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||
"generated": "Generated",
|
||||
"administration": "Administration",
|
||||
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
|
||||
"display_name_attribute": "Display Name Attribute",
|
||||
"display_name": "表示名",
|
||||
"configure_application_images": "アプリケーションの画像を設定",
|
||||
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.",
|
||||
"logo_from_url_description": "画像の直接のURL (svg, png, webp) を貼り付けます。アイコンは <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> か <link href=\"https://dashboardicons.com\">Dashboard Icons</link> で探せます。",
|
||||
"invalid_url": "無効な URL",
|
||||
"require_user_email": "Require Email Address",
|
||||
"require_user_email_description": "ユーザーにメールアドレスの登録を必須とします。無効にした場合、メールアドレスを持たないユーザーはメールアドレスが必要な機能を利用できなくなります。",
|
||||
"view": "表示",
|
||||
"toggle_columns": "列の表示/非表示を切り替える",
|
||||
"locale": "ロケール",
|
||||
"ldap_id": "LDAP ID",
|
||||
"reauthentication": "再認証",
|
||||
"clear_filters": "フィルターをクリア"
|
||||
}
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "이미지 다이렉트 URL (svg, png, webp)을 붙여넣으세요. <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> 또는 <link href=\"https://dashboardicons.com\">Dashboard Icons</link>에서 아이콘을 찾으세요.",
|
||||
"invalid_url": "잘못된 URL",
|
||||
"require_user_email": "이메일 주소 필수",
|
||||
"require_user_email_description": "사용자에게 이메일 주소가 있어야 합니다. 이 설정이 비활성화되면 이메일 주소가 없는 사용자는 이메일 주소가 필요한 기능을 사용할 수 없습니다."
|
||||
"require_user_email_description": "사용자에게 이메일 주소가 있어야 합니다. 이 설정이 비활성화되면 이메일 주소가 없는 사용자는 이메일 주소가 필요한 기능을 사용할 수 없습니다.",
|
||||
"view": "보기",
|
||||
"toggle_columns": "열 표시/숨기기",
|
||||
"locale": "로케일",
|
||||
"ldap_id": "LDAP ID",
|
||||
"reauthentication": "재인증",
|
||||
"clear_filters": "필터 지우기"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Plak een directe afbeeldings-URL (svg, png, webp). Zoek pictogrammen op <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> of <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Ongeldige URL",
|
||||
"require_user_email": "E-mailadres vereist",
|
||||
"require_user_email_description": "Je moet een e-mailadres hebben. Als je dit uitschakelt, kunnen mensen zonder e-mailadres geen functies gebruiken waarvoor een e-mailadres nodig is."
|
||||
"require_user_email_description": "Je moet een e-mailadres hebben. Als je dit uitschakelt, kunnen mensen zonder e-mailadres geen functies gebruiken waarvoor een e-mailadres nodig is.",
|
||||
"view": "Bekijken",
|
||||
"toggle_columns": "Kolommen wisselen",
|
||||
"locale": "Locatie",
|
||||
"ldap_id": "LDAP-ID",
|
||||
"reauthentication": "Opnieuw inloggen",
|
||||
"clear_filters": "Filters wissen"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Wklej bezpośredni adres URL obrazu (svg, png, webp). Znajdź ikony na stronie <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> lub <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Nieprawidłowy adres URL",
|
||||
"require_user_email": "Wymagany adres e-mail",
|
||||
"require_user_email_description": "Wymaga od was posiadania adresu e-mail. Jeśli opcja zostanie wyłączona, wy bez adresu e-mail nie będziecie mogli korzystać z funkcji wymagających adresu e-mail."
|
||||
"require_user_email_description": "Wymaga od was posiadania adresu e-mail. Jeśli opcja zostanie wyłączona, wy bez adresu e-mail nie będziecie mogli korzystać z funkcji wymagających adresu e-mail.",
|
||||
"view": "Widok",
|
||||
"toggle_columns": "Przełącz kolumny",
|
||||
"locale": "Ustawienia regionalne",
|
||||
"ldap_id": "Identyfikator LDAP",
|
||||
"reauthentication": "Ponowne uwierzytelnianie",
|
||||
"clear_filters": "Wyczyść filtry"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"click_profile_picture_to_upload_custom": "Clique na foto de perfil para enviar uma imagem personalizada dos seus arquivos.",
|
||||
"image_should_be_in_format": "A imagem deve estar no formato PNG ou JPEG.",
|
||||
"items_per_page": "Itens por página",
|
||||
"no_items_found": "Nenhum item encontrado",
|
||||
"no_items_found": "Nada foi encontrado",
|
||||
"select_items": "Selecione os itens...",
|
||||
"search": "Pesquisar...",
|
||||
"expand_card": "Expandir cartão",
|
||||
@@ -27,8 +27,8 @@
|
||||
"alternative_sign_in_methods": "Outras formas de entrar",
|
||||
"login_background": "Histórico de login",
|
||||
"logo": "Logotipo",
|
||||
"login_code": "Código de Login:",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Crie um código de login que o usuário possa usar para entrar sem precisar digitar uma senha.",
|
||||
"login_code": "Código de Login",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Crie um código de login de uso único para que o usuário possa entrar sem precisar de uma chave de acesso.",
|
||||
"one_hour": "1 hora",
|
||||
"twelve_hours": "12 horas",
|
||||
"one_day": "1 dia",
|
||||
@@ -38,52 +38,52 @@
|
||||
"generate_code": "Gerar Código",
|
||||
"name": "Nome",
|
||||
"browser_unsupported": "Navegador não suportado",
|
||||
"this_browser_does_not_support_passkeys": "Esse navegador não aceita chaves de acesso. Tenta usar outro jeito de entrar.",
|
||||
"this_browser_does_not_support_passkeys": "Este navegador não aceita chaves de acesso. Tente usar outro método de login.",
|
||||
"an_unknown_error_occurred": "Ocorreu um erro desconhecido",
|
||||
"authentication_process_was_aborted": "O processo de autenticação foi abortado",
|
||||
"error_occurred_with_authenticator": "Ocorreu um erro com o autenticador",
|
||||
"authenticator_does_not_support_discoverable_credentials": "O autenticador não suporta credenciais detectáveis",
|
||||
"authenticator_does_not_support_resident_keys": "O autenticador não aceita chaves residentes.",
|
||||
"passkey_was_previously_registered": "Essa chave mestra já foi registrada antes.",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "O autenticador não suporta nenhum dos algoritmos solicitados.",
|
||||
"authenticator_does_not_support_resident_keys": "O autenticador não aceita chaves residentes",
|
||||
"passkey_was_previously_registered": "Esta chave de acesso já está registrada",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "O autenticador não suporta nenhum dos algoritmos solicitados",
|
||||
"authenticator_timed_out": "Tempo limite do autenticador atingido",
|
||||
"critical_error_occurred_contact_administrator": "Ocorreu um erro grave. Por favor, entre em contato com o administrador.",
|
||||
"sign_in_to": "Entrar em {name}",
|
||||
"client_not_found": "Cliente não encontrado",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> quer acessar as seguintes informações:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Você quer entrar em <b>{client}</b> com a sua conta {appName}?",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Deseja entrar em <b>{client}</b> com a sua conta {appName}?",
|
||||
"email": "E-mail",
|
||||
"view_your_email_address": "Ver seu endereço de e-mail",
|
||||
"profile": "Perfil",
|
||||
"view_your_profile_information": "Dá uma olhada nas informações do seu perfil",
|
||||
"view_your_profile_information": "Veja as informações do seu perfil",
|
||||
"groups": "Grupos",
|
||||
"view_the_groups_you_are_a_member_of": "Dá uma olhada nos grupos que você faz parte",
|
||||
"view_the_groups_you_are_a_member_of": "Veja os grupos que você faz parte",
|
||||
"cancel": "Cancelar",
|
||||
"sign_in": "Entrar",
|
||||
"try_again": "Tentar novamente",
|
||||
"client_logo": "Logo do Cliente",
|
||||
"sign_out": "Sair",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Deseja sair de {appName} com a conta <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Entrar em {appName}",
|
||||
"please_try_to_sign_in_again": "Tenta entrar de novo.",
|
||||
"authenticate_with_passkey_to_access_account": "Autentique-se com sua chave de acesso para entrar na sua conta.",
|
||||
"please_try_to_sign_in_again": "Tente entrar novamente.",
|
||||
"authenticate_with_passkey_to_access_account": "Identifique-se com sua chave de acesso para entrar em sua conta.",
|
||||
"authenticate": "Autenticar",
|
||||
"please_try_again": "Tenta de novo, por favor.",
|
||||
"please_try_again": "Por favor, tente novamente.",
|
||||
"continue": "Continuar",
|
||||
"alternative_sign_in": "Entrar de outra forma",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Se você não tem acesso à sua chave de acesso, pode entrar usando um dos métodos a seguir.",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Se perdeu sua chave de acesso, pode entrar usando um dos métodos a seguir.",
|
||||
"use_your_passkey_instead": "Quer usar sua chave de acesso?",
|
||||
"email_login": "Entrar com e-mail",
|
||||
"enter_a_login_code_to_sign_in": "Digite um código de login para entrar.",
|
||||
"sign_in_with_login_code": "Faça login com o código de acesso",
|
||||
"request_a_login_code_via_email": "Pede um código de login por e-mail.",
|
||||
"request_a_login_code_via_email": "Solicitar um código de login por e-mail.",
|
||||
"go_back": "Voltar",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Mandamos um e-mail pro endereço que você deu, se ele estiver no nosso sistema.",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "E-mail enviado para o endereço fornecido, se ele existir no sistema.",
|
||||
"enter_code": "Digite o código",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Digite seu e-mail pra receber um e-mail com um código de login.",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Digite seu e-mail para receber um código de login.",
|
||||
"your_email": "Seu e-mail",
|
||||
"submit": "Enviar",
|
||||
"enter_the_code_you_received_to_sign_in": "Digite o código que você recebeu para entrar.",
|
||||
"enter_the_code_you_received_to_sign_in": "Para entrar, digite o código que recebeu.",
|
||||
"code": "Código",
|
||||
"invalid_redirect_url": "URL de redirecionamento inválido",
|
||||
"audit_log": "Registro de Auditoria",
|
||||
@@ -95,7 +95,7 @@
|
||||
"settings": "Configurações",
|
||||
"update_pocket_id": "Atualizar Pocket ID",
|
||||
"powered_by": "Fornecido por",
|
||||
"see_your_account_activities_from_the_last_3_months": "Veja suas atividades de conta dos últimos 3 meses.",
|
||||
"see_your_account_activities_from_the_last_3_months": "Veja as atividades da conta nos últimos 3 meses.",
|
||||
"time": "Hora",
|
||||
"event": "Evento",
|
||||
"approximate_location": "Localização Aproximada",
|
||||
@@ -104,34 +104,34 @@
|
||||
"client": "Cliente",
|
||||
"unknown": "Desconhecido",
|
||||
"account_details_updated_successfully": "Detalhes da conta atualizados com sucesso",
|
||||
"profile_picture_updated_successfully": "Foto do perfil atualizada com sucesso. Pode demorar alguns minutos para atualizar.",
|
||||
"profile_picture_updated_successfully": "Foto de perfil alterada com sucesso. A atualização pode demorar alguns minutos.",
|
||||
"account_settings": "Configurações de Conta",
|
||||
"passkey_missing": "Chave de acesso ausente",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Adicione uma senha para evitar perder o acesso à sua conta.",
|
||||
"single_passkey_configured": "Chave única configurada",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "É melhor adicionar mais de uma senha de acesso pra evitar perder o acesso à sua conta.",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Adicione uma chave de acesso para evitar perder o acesso à sua conta.",
|
||||
"single_passkey_configured": "Uma chave de acesso configurada",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "É recomendado adicionar mais de uma chave de acesso para evitar perder o acesso à sua conta.",
|
||||
"account_details": "Detalhes da Conta",
|
||||
"passkeys": "Chaves-mestras",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gerencie as chaves de acesso que você pode usar para se autenticar.",
|
||||
"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.",
|
||||
"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 de uma senha.",
|
||||
"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",
|
||||
"first_name": "Primeiro nome",
|
||||
"last_name": "Último nome",
|
||||
"first_name": "Nome",
|
||||
"last_name": "Sobrenome",
|
||||
"username": "Nome de usuário",
|
||||
"save": "Salvar",
|
||||
"username_can_only_contain": "O nome de usuário só pode conter letras minúsculas, números, underscores, pontos, hífens e símbolos '@'",
|
||||
"username_must_start_with": "O nome de usuário precisa começar com um caractere alfanumérico.",
|
||||
"username_must_end_with": "O nome de usuário precisa terminar com um caractere alfanumérico.",
|
||||
"username_can_only_contain": "O nome de usuário só pode conter letras minúsculas, números, subtraço, pontos, hifens e símbolos '@'",
|
||||
"username_must_start_with": "O nome de usuário precisa começar com um caractere alfanumérico",
|
||||
"username_must_end_with": "O nome de usuário precisa terminar com um caractere alfanumérico",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Faça o login usando o código a seguir. O código irá expirar em 15 minutos.",
|
||||
"or_visit": "ou visite",
|
||||
"added_on": "Adicionado em",
|
||||
"rename": "Renomear",
|
||||
"delete": "Apagar",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Tem certeza que quer apagar essa chave de acesso?",
|
||||
"passkey_deleted_successfully": "Chave de acesso apagada com sucesso",
|
||||
"delete_passkey_name": "Apagar {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Nome da chave de acesso atualizado com sucesso",
|
||||
"delete": "Deletar",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Tem certeza que deseja deletar esta chave de acesso?",
|
||||
"passkey_deleted_successfully": "Chave de acesso deletada com sucesso",
|
||||
"delete_passkey_name": "Deletar {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Nome da chave de acesso atualizada com sucesso",
|
||||
"name_passkey": "Nome da chave de acesso",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Dê um nome à sua chave de acesso para identificá-la facilmente mais tarde.",
|
||||
"create_api_key": "Criar chave API",
|
||||
@@ -139,20 +139,20 @@
|
||||
"add_api_key": "Adicionar chave API",
|
||||
"manage_api_keys": "Gerenciar chaves API",
|
||||
"api_key_created": "Chave API criada",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Por segurança, essa chave só vai aparecer uma vez. Guarde-a em um lugar seguro.",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Por segurança, esta chave só será exibida uma vez. Salve em um lugar seguro.",
|
||||
"description": "Descrição",
|
||||
"api_key": "Chave API",
|
||||
"close": "Fechar",
|
||||
"name_to_identify_this_api_key": "Nome pra identificar essa chave API.",
|
||||
"expires_at": "Vence em",
|
||||
"when_this_api_key_will_expire": "Quando essa chave API vai expirar.",
|
||||
"name_to_identify_this_api_key": "Nome para identificar esta chave API.",
|
||||
"expires_at": "Valido até",
|
||||
"when_this_api_key_will_expire": "Quando esta chave API irá expirar.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Descrição opcional para ajudar a identificar a finalidade desta chave.",
|
||||
"expiration_date_must_be_in_the_future": "A data de validade precisa ser no futuro.",
|
||||
"expiration_date_must_be_in_the_future": "A data de validade precisa ser no futuro",
|
||||
"revoke_api_key": "Revogar chave API",
|
||||
"never": "Nunca",
|
||||
"revoke": "Revogar",
|
||||
"api_key_revoked_successfully": "Chave API revogada com sucesso",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Tem certeza que quer cancelar a chave API “{apiKeyName}”? Isso vai desligar todas as integrações que usam essa chave.",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Tem certeza que deseja cancelar a chave API “{apiKeyName}”? Isto irá desligar todas as integrações que usam esta chave.",
|
||||
"last_used": "Último uso",
|
||||
"actions": "Ações",
|
||||
"images_updated_successfully": "Imagens atualizadas com sucesso",
|
||||
@@ -164,10 +164,10 @@
|
||||
"update": "Atualização",
|
||||
"email_configuration_updated_successfully": "Configuração do e-mail atualizada com sucesso",
|
||||
"save_changes_question": "Salvar alterações?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Você precisa salvar as alterações antes de enviar um e-mail de teste. Quer salvar agora?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "É necessário salvar as alterações antes de enviar um e-mail de teste. Deseja salvar agora?",
|
||||
"save_and_send": "Salvar e enviar",
|
||||
"test_email_sent_successfully": "E-mail de teste enviado com sucesso para o seu endereço de e-mail.",
|
||||
"failed_to_send_test_email": "Não deu certo enviar o e-mail de teste. Dá uma olhada nos registros do servidor pra saber mais.",
|
||||
"failed_to_send_test_email": "Falha ao enviar o e-mail de teste. Verifique os registros no servidor para mais detalhes.",
|
||||
"smtp_configuration": "Configuração SMTP",
|
||||
"smtp_host": "Host SMTP",
|
||||
"smtp_port": "Porta SMTP",
|
||||
@@ -177,21 +177,21 @@
|
||||
"smtp_tls_option": "Opção SMTP TLS",
|
||||
"email_tls_option": "Opção TLS para e-mail",
|
||||
"skip_certificate_verification": "Pular a verificação do certificado",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Isso pode ser útil para certificados autoassinados.",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Isto pode ser útil para certificados autoassinados.",
|
||||
"enabled_emails": "E-mails ativados",
|
||||
"email_login_notification": "Notificação de login por e-mail",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Manda um e-mail pro usuário quando ele entrar com um novo aparelho.",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Envia um e-mail para o usuário quando ele entrar com um novo dispositivo.",
|
||||
"emai_login_code_requested_by_user": "Código de login por e-mail solicitado pelo usuário",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permite que os usuários pulem as senhas pedindo um código de login que vai pro e-mail deles. Isso deixa a segurança bem mais fraca, já que qualquer um que tiver acesso ao e-mail do usuário pode entrar.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permite que os usuários ignorem suas chaves de acesso, pedindo um código de login diretamente no e-mail do usuário. Isso diminui a segurança, já que qualquer um com acesso ao e-mail do usuário pode entrar em sua conta.",
|
||||
"email_login_code_from_admin": "Código de login por e-mail do administrador",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Permite que um administrador envie um código de login pro usuário por e-mail.",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Permite que um administrador envie um código de login para o e-mail do usuário.",
|
||||
"send_test_email": "Enviar e-mail de teste",
|
||||
"application_configuration_updated_successfully": "A configuração do aplicativo foi atualizada com sucesso.",
|
||||
"application_configuration_updated_successfully": "A configuração do aplicativo foi atualizada com sucesso",
|
||||
"application_name": "Nome do aplicativo",
|
||||
"session_duration": "Duração da sessão",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "O tempo que dura uma sessão, em minutos, antes que o usuário precise fazer login de novo.",
|
||||
"enable_self_account_editing": "Ativar edição da conta pessoal",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Se os usuários podem editar os detalhes da conta deles.",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Se os usuários podem editar os detalhes de suas contas.",
|
||||
"emails_verified": "E-mails verificados",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Se o e-mail do usuário deve ser marcado como verificado para os clientes OIDC.",
|
||||
"ldap_configuration_updated_successfully": "Configuração LDAP atualizada com sucesso",
|
||||
@@ -199,41 +199,41 @@
|
||||
"ldap_sync_finished": "Sincronização LDAP concluída",
|
||||
"client_configuration": "Configuração do cliente",
|
||||
"ldap_url": "URL LDAP",
|
||||
"ldap_bind_dn": "DN de ligação LDAP",
|
||||
"ldap_bind_password": "Senha de ligação LDAP",
|
||||
"ldap_base_dn": "DN base LDAP",
|
||||
"ldap_bind_dn": "LDAP vincular DN",
|
||||
"ldap_bind_password": "LDAP vincular Senha",
|
||||
"ldap_base_dn": "LDAP DN base",
|
||||
"user_search_filter": "Filtro de pesquisa de usuários",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "O filtro de pesquisa que você usa pra procurar/sincronizar usuários.",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "O filtro de pesquisa utilizado para encontrar/sincronizar usuários.",
|
||||
"groups_search_filter": "Filtro de pesquisa de grupos",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "O filtro de pesquisa que você usa pra procurar/sincronizar grupos.",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "O filtro de pesquisa utilizado para encontrar/sincronizar grupos.",
|
||||
"attribute_mapping": "Mapeamento de atributos",
|
||||
"user_unique_identifier_attribute": "Atributo de identificador único do usuário",
|
||||
"the_value_of_this_attribute_should_never_change": "O valor desse atributo nunca deve mudar.",
|
||||
"the_value_of_this_attribute_should_never_change": "O valor deste atributo nunca deve mudar.",
|
||||
"username_attribute": "Atributo do nome de usuário",
|
||||
"user_mail_attribute": "Atributo de e-mail do usuário",
|
||||
"user_first_name_attribute": "Atributo do primeiro nome do usuário",
|
||||
"user_last_name_attribute": "Atributo do sobrenome do usuário",
|
||||
"user_profile_picture_attribute": "Atributo da foto do perfil do usuário",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "O valor desse atributo pode ser uma URL, um binário ou uma imagem codificada em base64.",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "O valor deste atributo pode ser uma URL, um binário ou uma imagem codificada em base64.",
|
||||
"group_members_attribute": "Atributo dos membros do grupo",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "O atributo a ser usado para consultar membros de um grupo.",
|
||||
"group_unique_identifier_attribute": "Atributo identificador exclusivo do grupo",
|
||||
"group_rdn_attribute": "Atributo RDN do grupo (em DN)",
|
||||
"admin_group_name": "Nome do grupo de administradores",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Os membros desse grupo vão ter privilégios de administrador no Pocket ID.",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Os membros deste grupo terão privilégios de administrador no Pocket ID.",
|
||||
"disable": "Desativar",
|
||||
"sync_now": "Sincronizar agora",
|
||||
"enable": "Ativar",
|
||||
"user_created_successfully": "Usuário criado com sucesso",
|
||||
"create_user": "Criar Usuário",
|
||||
"add_a_new_user_to_appname": "Adicionar um novo usuário para {appName}",
|
||||
"add_a_new_user_to_appname": "Adicionar um novo usuário em {appName}",
|
||||
"add_user": "Adicionar Usuário",
|
||||
"manage_users": "Gerenciar Usuários",
|
||||
"admin_privileges": "Privilégios de administrador",
|
||||
"admins_have_full_access_to_the_admin_panel": "Os administradores têm acesso total ao painel de administração.",
|
||||
"delete_firstname_lastname": "Apagar {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Tem certeza que quer excluir esse usuário?",
|
||||
"user_deleted_successfully": "Usuário excluído com sucesso",
|
||||
"delete_firstname_lastname": "Deletar {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Tem certeza que deseja deletar este usuário?",
|
||||
"user_deleted_successfully": "Usuário deletado com sucesso",
|
||||
"role": "Função",
|
||||
"source": "Fonte",
|
||||
"admin": "Admin",
|
||||
@@ -243,51 +243,51 @@
|
||||
"edit": "Editar",
|
||||
"user_groups_updated_successfully": "Grupos de usuários atualizados com sucesso",
|
||||
"user_updated_successfully": "Usuário atualizado com sucesso",
|
||||
"custom_claims_updated_successfully": "Reclamações personalizadas atualizadas com sucesso",
|
||||
"custom_claims_updated_successfully": "Reivindicações personalizadas atualizadas com sucesso",
|
||||
"back": "Voltar",
|
||||
"user_details_firstname_lastname": "Detalhes do usuário {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Controle quais grupos esse usuário faz parte.",
|
||||
"custom_claims": "Reclamações personalizadas",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "As reivindicações personalizadas são pares de chave-valor que podem ser usados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “perfil” for solicitado.",
|
||||
"manage_which_groups_this_user_belongs_to": "Gerencie os grupos deste usuário.",
|
||||
"custom_claims": "Reivindicações personalizadas",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "As reivindicações personalizadas são pares de chave-valor que podem ser utilizados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “profile” for solicitado.",
|
||||
"user_group_created_successfully": "Grupo de usuários criado com sucesso",
|
||||
"create_user_group": "Criar Grupo de Usuários",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Crie um novo grupo que possa ser atribuído aos usuários.",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Crie um grupo que possa ser atribuído aos usuários.",
|
||||
"add_group": "Adicionar Grupo",
|
||||
"manage_user_groups": "Gerenciar grupos de usuários",
|
||||
"friendly_name": "Nome Amigável",
|
||||
"name_that_will_be_displayed_in_the_ui": "Nome que vai aparecer na interface do usuário",
|
||||
"name_that_will_be_in_the_groups_claim": "Nome que vai aparecer na reivindicação “grupos”",
|
||||
"delete_name": "Apagar {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Tem certeza que quer excluir esse grupo de usuários?",
|
||||
"user_group_deleted_successfully": "Grupo de usuários excluído com sucesso",
|
||||
"user_count": "Contagem de usuários",
|
||||
"name_that_will_be_in_the_groups_claim": "Nome que vai aparecer na reivindicação “groups”",
|
||||
"delete_name": "Deletar {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Tem certeza que deseja deletar este grupo de usuários?",
|
||||
"user_group_deleted_successfully": "Grupo de usuários deletado com sucesso",
|
||||
"user_count": "Total de usuários",
|
||||
"user_group_updated_successfully": "Grupo de usuários atualizado com sucesso",
|
||||
"users_updated_successfully": "Usuários atualizados com sucesso",
|
||||
"user_group_details_name": "Detalhes do grupo de usuários {name}",
|
||||
"assign_users_to_this_group": "Adicione usuários a este grupo.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "As reivindicações personalizadas são pares de chave-valor que podem ser usados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “perfil” for solicitado. As reivindicações personalizadas definidas no usuário serão priorizadas se houver conflitos.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "As reivindicações personalizadas são pares de chave-valor que podem ser utilizados para guardar informações adicionais sobre um usuário. Essas reivindicações serão incluídas no token de identificação se o escopo “profile” for solicitado. As reivindicações personalizadas definidas no usuário serão priorizadas se houver conflitos.",
|
||||
"oidc_client_created_successfully": "Cliente OIDC criado com sucesso",
|
||||
"create_oidc_client": "Criar cliente OIDC",
|
||||
"add_a_new_oidc_client_to_appname": "Adicione um novo cliente OIDC em {appName}.",
|
||||
"add_oidc_client": "Adicionar cliente OIDC",
|
||||
"manage_oidc_clients": "Gerenciar clientes OIDC",
|
||||
"one_time_link": "Link único",
|
||||
"one_time_link": "Link de uso único",
|
||||
"use_this_link_to_sign_in_once": "Use este link para fazer login uma vez. Isso é necessário para usuários que ainda não adicionaram uma chave de acesso ou que a perderam.",
|
||||
"add": "Adicionar",
|
||||
"callback_urls": "URLs de retorno de chamada",
|
||||
"logout_callback_urls": "URLs de retorno de chamada de logout",
|
||||
"public_client": "Cliente Público",
|
||||
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"public_clients_description": "Os clientes públicos não têm um segredo de cliente. Eles são feitos para aplicativos móveis, web e nativos, onde os segredos não podem ser guardados com segurança.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "A troca de chaves públicas é um recurso de segurança que evita ataques CSRF e interceptação de códigos de autorização.",
|
||||
"requires_reauthentication": "Precisa autenticar de novo",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Pede que os usuários se autentiquem de novo em cada autorização, mesmo que já estejam conectados.",
|
||||
"requires_reauthentication": "Obrigatório autenticar novamente",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Pede que os usuários se autentiquem novamente em cada autorização, mesmo que já estejam conectados",
|
||||
"name_logo": "{name} logotipo",
|
||||
"change_logo": "Alterar logotipo",
|
||||
"upload_logo": "Carregar logotipo",
|
||||
"remove_logo": "Tirar o logotipo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Tem certeza que quer apagar esse cliente OIDC?",
|
||||
"oidc_client_deleted_successfully": "Cliente OIDC excluído com sucesso",
|
||||
"remove_logo": "Remover o logotipo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Tem certeza que deseja deletar este cliente OIDC?",
|
||||
"oidc_client_deleted_successfully": "Cliente OIDC deletado com sucesso",
|
||||
"authorization_url": "URL de autorização",
|
||||
"oidc_discovery_url": "URL de descoberta OIDC",
|
||||
"token_url": "URL do token",
|
||||
@@ -295,10 +295,10 @@
|
||||
"logout_url": "URL de logout",
|
||||
"certificate_url": "URL do certificado",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Deficientes",
|
||||
"disabled": "Desativado",
|
||||
"oidc_client_updated_successfully": "Cliente OIDC atualizado com sucesso",
|
||||
"create_new_client_secret": "Criar novo segredo do cliente",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Tem certeza que quer criar um novo segredo de cliente? O antigo vai ser invalidado.",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Tem certeza que deseja criar um segredo de cliente? O antigo será invalidado.",
|
||||
"generate": "Gerar",
|
||||
"new_client_secret_created_successfully": "Novo segredo do cliente criado com sucesso",
|
||||
"allowed_user_groups_updated_successfully": "Grupos de usuários permitidos atualizados com sucesso",
|
||||
@@ -307,19 +307,19 @@
|
||||
"client_secret": "Segredo do cliente",
|
||||
"show_more_details": "Mostrar mais detalhes",
|
||||
"allowed_user_groups": "Grupos de usuários permitidos",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Adicione grupos de usuários a este cliente para restringir o acesso aos usuários desses grupos. Se nenhum grupo de usuários for selecionado, todos os usuários terão acesso a este cliente.",
|
||||
"favicon": "Favicon",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Adicione grupos de usuários a este cliente para restringir o acesso somente aos usuários que fazem parte destes grupos. Se nenhum grupo de usuários for selecionado, todos os usuários terão acesso a este cliente.",
|
||||
"favicon": "Ícone de Favorito",
|
||||
"light_mode_logo": "Logotipo do modo claro",
|
||||
"dark_mode_logo": "Logotipo do Modo Escuro",
|
||||
"background_image": "Imagem de fundo",
|
||||
"language": "Idioma",
|
||||
"reset_profile_picture_question": "Queres redefinir a tua foto de perfil?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Isso vai tirar a foto que você mandou e voltar a foto do perfil pro padrão. Quer mesmo continuar?",
|
||||
"reset_profile_picture_question": "Redefinir a foto de perfil?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Isto irá remover a foto atual e a imagem de perfil padrão será utilizada. Deseja continuar?",
|
||||
"reset": "Redefinir",
|
||||
"reset_to_default": "Redefinir para o padrão",
|
||||
"profile_picture_has_been_reset": "A foto do perfil foi redefinida. A atualização pode demorar alguns minutos.",
|
||||
"select_the_language_you_want_to_use": "Escolha o idioma que você quer usar. Lembre-se de que alguns textos podem ser traduzidos automaticamente e podem não estar certos.",
|
||||
"contribute_to_translation": "Se você encontrar algum problema, pode ajudar na tradução no <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"select_the_language_you_want_to_use": "Escolha o idioma que deseja usar. Lembre-se de que alguns textos podem ser traduzidos automaticamente e podem não estar certos.",
|
||||
"contribute_to_translation": "Se encontrar algum problema, ajude na tradução no <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Pessoal",
|
||||
"global": "Global",
|
||||
"all_users": "Todos os usuários",
|
||||
@@ -327,42 +327,42 @@
|
||||
"all_clients": "Todos os clientes",
|
||||
"all_locations": "Todos os locais",
|
||||
"global_audit_log": "Registro de auditoria global",
|
||||
"see_all_account_activities_from_the_last_3_months": "Dá uma olhada em tudo que os usuários fizeram nos últimos 3 meses.",
|
||||
"see_all_account_activities_from_the_last_3_months": "Veja todas as atividades de todos os usuários nos últimos 3 meses.",
|
||||
"token_sign_in": "Entrar com token",
|
||||
"client_authorization": "Autorização do cliente",
|
||||
"new_client_authorization": "Autorização de novo cliente",
|
||||
"disable_animations": "Desativar animações",
|
||||
"turn_off_ui_animations": "Desligue as animações em toda a interface do usuário.",
|
||||
"user_disabled": "Conta desativada",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Usuários com deficiência não conseguem fazer login ou usar os serviços.",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Usuários inativos não conseguem fazer login ou usar os serviços.",
|
||||
"user_disabled_successfully": "O usuário foi desativado com sucesso.",
|
||||
"user_enabled_successfully": "O usuário foi ativado com sucesso.",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "Desativar {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Tem certeza que quer desativar esse usuário? Ele não vai conseguir entrar nem acessar nenhum serviço.",
|
||||
"ldap_soft_delete_users": "Impedir que usuários desativados acessem o LDAP.",
|
||||
"ldap_soft_delete_users_description": "Quando ativada, os usuários removidos do LDAP serão desativados em vez de excluídos do sistema.",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Tem certeza que deseja desativar este usuário? Não será mais possível entrar nem acessar os serviços.",
|
||||
"ldap_soft_delete_users": "Manter usuários desativados no LDAP.",
|
||||
"ldap_soft_delete_users_description": "Quando ativado, usuários removidos do LDAP serão apenas desativados ao invés de deletados do sistema.",
|
||||
"login_code_email_success": "O código de login foi enviado para o usuário.",
|
||||
"send_email": "Enviar e-mail",
|
||||
"show_code": "Mostrar código",
|
||||
"callback_url_description": "URL(s) fornecido(s) pelo seu cliente. Vai ser adicionado automaticamente se você deixar em branco. Caracteres curinga (*) são aceitos, mas é melhor evitar para garantir mais segurança.",
|
||||
"logout_callback_url_description": "URL(s) que seu cliente deu pra sair da conta. Você pode usar curingas (*), mas é melhor evitar pra ficar mais seguro.",
|
||||
"callback_url_description": "URL(s) providenciados pelo cliente. Será adicionado automaticamente se deixar em branco. Apesar de caracteres curinga (*) serem aceitos, para uma segurança maior, não é recomendado utiliza-los.",
|
||||
"logout_callback_url_description": "URL(s) providenciados pelo cliente para sair da conta. Apesar de curingas (*) serem aceitos, para uma segurança maior, não é recomendado utiliza-los.",
|
||||
"api_key_expiration": "Expiração da chave API",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Manda um e-mail pro usuário quando a chave API dele estiver quase a expirar.",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envia um e-mail para o usuário quando a chave API estiver próxima de expirar.",
|
||||
"authorize_device": "Autorizar dispositivo",
|
||||
"the_device_has_been_authorized": "O dispositivo foi autorizado.",
|
||||
"enter_code_displayed_in_previous_step": "Digite o código que apareceu na etapa anterior.",
|
||||
"authorize": "Autorizar",
|
||||
"federated_client_credentials": "Credenciais de Cliente Federadas",
|
||||
"federated_client_credentials_description": "Usando credenciais de cliente federadas, você pode autenticar clientes OIDC usando tokens JWT emitidos por autoridades de terceiros.",
|
||||
"federated_client_credentials_description": "Ao utilizar credenciais de cliente federadas, é possível autenticar clientes OIDC usando tokens JWT emitidos por autoridades de terceiros.",
|
||||
"add_federated_client_credential": "Adicionar credencial de cliente federado",
|
||||
"add_another_federated_client_credential": "Adicionar outra credencial de cliente federado",
|
||||
"oidc_allowed_group_count": "Contagem de grupos permitidos",
|
||||
"oidc_allowed_group_count": "Total de grupos permitidos",
|
||||
"unrestricted": "Sem restrições",
|
||||
"show_advanced_options": "Mostrar opções avançadas",
|
||||
"hide_advanced_options": "Ocultar opções avançadas",
|
||||
"oidc_data_preview": "Pré-visualização dos dados OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Dá uma olhada nos dados OIDC que seriam enviados para diferentes usuários",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Veja quais os dados OIDC que seriam enviados por diferentes usuários",
|
||||
"id_token": "Token de identificação",
|
||||
"access_token": "Token de acesso",
|
||||
"userinfo": "Informações do usuário",
|
||||
@@ -370,46 +370,46 @@
|
||||
"access_token_payload": "Carga útil do token de acesso",
|
||||
"userinfo_endpoint_response": "Resposta do ponto final da informação do usuário",
|
||||
"copy": "Copiar",
|
||||
"no_preview_data_available": "Não tem dados de pré-visualização disponíveis",
|
||||
"no_preview_data_available": "Não há dados de pré-visualização disponíveis",
|
||||
"copy_all": "Copiar tudo",
|
||||
"preview": "Pré-visualização",
|
||||
"preview_for_user": "Prévia de “ {name} ”",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Dá uma olhada nos dados OIDC que seriam enviados para esse usuário.",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Veja os dados OIDC que seriam enviados para este usuário",
|
||||
"show": "Mostrar",
|
||||
"select_an_option": "Escolha uma opção",
|
||||
"select_an_option": "Selecione uma opção",
|
||||
"select_user": "Selecionar usuário",
|
||||
"error": "Erro",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Escolha uma cor de destaque pra personalizar a aparência do Pocket ID.",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Escolha uma cor de destaque para personalizar a aparência do Pocket ID.",
|
||||
"accent_color": "Cor de destaque",
|
||||
"custom_accent_color": "Cor de destaque personalizada",
|
||||
"custom_accent_color_description": "Digite uma cor personalizada usando formatos de cor CSS válidos (por exemplo, hex, rgb, hsl).",
|
||||
"color_value": "Valor da cor",
|
||||
"apply": "Inscreva-se",
|
||||
"apply": "Aplicar",
|
||||
"signup_token": "Token de inscrição",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "Crie um token de inscrição para permitir o registro de novos usuários.",
|
||||
"usage_limit": "Limite de uso",
|
||||
"number_of_times_token_can_be_used": "Número de vezes que o token de inscrição pode ser usado.",
|
||||
"expires": "Vence",
|
||||
"expires": "Valido até",
|
||||
"signup": "Cadastre-se",
|
||||
"user_creation": "Criação de usuário",
|
||||
"configure_user_creation": "Gerencie as configurações de criação de usuários, incluindo métodos de inscrição e permissões padrão para novos usuários.",
|
||||
"user_creation_groups_description": "Atribuir esses grupos automaticamente aos novos usuários quando eles se cadastrarem.",
|
||||
"user_creation_claims_description": "Atribua essas reivindicações personalizadas automaticamente aos novos usuários no momento da inscrição.",
|
||||
"user_creation": "Criar usuário",
|
||||
"configure_user_creation": "Gerencie as configurações da criação de usuários, incluindo métodos de inscrição e permissões padrão para novos usuários.",
|
||||
"user_creation_groups_description": "Atribuir esses grupos automaticamente aos novos usuários no momento da inscrição.",
|
||||
"user_creation_claims_description": "Atribua estas reivindicações personalizadas automaticamente aos novos usuários no momento da inscrição.",
|
||||
"user_creation_updated_successfully": "As configurações de criação do usuário foram atualizadas com sucesso.",
|
||||
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.",
|
||||
"signup_requires_valid_token": "É preciso um token de inscrição válido pra criar uma conta.",
|
||||
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar contas de usuário.",
|
||||
"signup_requires_valid_token": "É necessário um token de inscrição válido para criar uma conta",
|
||||
"validating_signup_token": "Validando o token de inscrição",
|
||||
"go_to_login": "Vá para o login",
|
||||
"signup_to_appname": "Cadastre-se em {appName}",
|
||||
"create_your_account_to_get_started": "Crie sua conta pra começar.",
|
||||
"initial_account_creation_description": "Crie sua conta pra começar. Você vai poder definir uma senha mais tarde.",
|
||||
"create_your_account_to_get_started": "Crie sua conta para começar.",
|
||||
"initial_account_creation_description": "Crie sua conta para começar. Será possível cadastrar uma chave de acesso mais tarde.",
|
||||
"setup_your_passkey": "Configure sua chave de acesso",
|
||||
"create_a_passkey_to_securely_access_your_account": "Crie uma senha para acessar sua conta com segurança. Essa vai ser sua principal forma de entrar.",
|
||||
"create_a_passkey_to_securely_access_your_account": "Cadastre uma chave de acesso para entrar em sua conta com segurança. Esta será sua principal forma de entrar.",
|
||||
"skip_for_now": "Pular por enquanto",
|
||||
"account_created": "Conta criada",
|
||||
"enable_user_signups": "Ativar inscrições de usuários",
|
||||
"enable_user_signups_description": "Decida como os usuários podem se cadastrar para novas contas no Pocket ID.",
|
||||
"user_signups_are_disabled": "As inscrições de usuários estão desativadas no momento.",
|
||||
"user_signups_are_disabled": "A inscrição de novos usuários está desativada no momento",
|
||||
"create_signup_token": "Criar token de inscrição",
|
||||
"view_active_signup_tokens": "Ver tokens de inscrição ativos",
|
||||
"manage_signup_tokens": "Gerenciar tokens de inscrição",
|
||||
@@ -423,27 +423,27 @@
|
||||
"token": "Token",
|
||||
"loading": "Carregando",
|
||||
"delete_signup_token": "Apagar token de inscrição",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que quer apagar esse token de inscrição? Não dá pra voltar atrás.",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que deseja deletar este token de inscrição? Não é possível desfazer esta ação.",
|
||||
"signup_with_token": "Cadastre-se com token",
|
||||
"signup_with_token_description": "Os usuários só podem se cadastrar usando um token de cadastro válido criado por um administrador.",
|
||||
"signup_open": "Inscrição aberta",
|
||||
"signup_open_description": "Qualquer pessoa pode criar uma conta nova sem restrições.",
|
||||
"signup_open_description": "Qualquer pessoa pode criar uma conta nova, sem restrições.",
|
||||
"of": "de",
|
||||
"skip_passkey_setup": "Pular configuração da chave de acesso",
|
||||
"skip_passkey_setup_description": "É super recomendável criar uma senha de acesso, porque sem ela você vai ficar sem poder entrar na sua conta assim que a sessão acabar.",
|
||||
"skip_passkey_setup_description": "É altamente recomendado cadastrar uma chave de acesso, sem uma, não será possível entrar em sua conta novamente quando esta sessão acabar.",
|
||||
"my_apps": "Meus aplicativos",
|
||||
"no_apps_available": "Não tem aplicativos disponíveis",
|
||||
"contact_your_administrator_for_app_access": "Fala com o seu administrador pra conseguir acesso aos aplicativos.",
|
||||
"no_apps_available": "Não há aplicativos disponíveis",
|
||||
"contact_your_administrator_for_app_access": "Fale com o seu administrador para conseguir acesso aos aplicativos.",
|
||||
"launch": "Lançamento",
|
||||
"client_launch_url": "URL de lançamento do cliente",
|
||||
"client_launch_url_description": "A URL que vai abrir quando alguém abrir o aplicativo na página Meus aplicativos.",
|
||||
"client_launch_url_description": "A URL carregada quando um usuário abrir o aplicativo na página Meus aplicativos.",
|
||||
"client_name_description": "O nome do cliente que aparece na interface do Pocket ID.",
|
||||
"revoke_access": "Revogar acesso",
|
||||
"revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não vai mais conseguir acessar as informações da sua conta.",
|
||||
"revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não conseguirá mais acessar as informações da sua conta.",
|
||||
"revoke_access_successful": "O acesso a {clientName} foi revogado com sucesso.",
|
||||
"last_signed_in_ago": "Último login em {time} atrás",
|
||||
"invalid_client_id": "A ID do cliente só pode ter letras, números, sublinhados e hífens.",
|
||||
"custom_client_id_description": "Defina um ID de cliente personalizado se for necessário para o seu aplicativo. Caso contrário, deixe em branco para gerar um aleatório.",
|
||||
"last_signed_in_ago": "Último login à {time} atrás",
|
||||
"invalid_client_id": "A ID do cliente só pode ter letras, números, sub traços e hifens",
|
||||
"custom_client_id_description": "Se requerido pelo seu aplicativo, defina um ID de cliente personalizado. Caso contrário, deixe em branco para gerar um aleatório.",
|
||||
"generated": "Gerado",
|
||||
"administration": "Administração",
|
||||
"group_rdn_attribute_description": "O atributo usado no nome distinto (DN) dos grupos.",
|
||||
@@ -451,9 +451,15 @@
|
||||
"display_name": "Nome de exibição",
|
||||
"configure_application_images": "Configurar imagens de aplicativos",
|
||||
"ui_config_disabled_info_title": "Configuração da interface do usuário desativada",
|
||||
"ui_config_disabled_info_description": "A configuração da interface do usuário está desativada porque as configurações do aplicativo são gerenciadas por meio de variáveis de ambiente. Algumas configurações podem não ser editáveis.",
|
||||
"ui_config_disabled_info_description": "A configuração da interface do usuário está desativada porque as configurações do aplicativo são gerenciadas através de variáveis de ambiente. Algumas configurações podem não ser editáveis.",
|
||||
"logo_from_url_description": "Cole uma URL direta da imagem (svg, png, webp). Encontre ícones em <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "URL inválido",
|
||||
"require_user_email": "É preciso um endereço de e-mail",
|
||||
"require_user_email_description": "Pede que os usuários tenham um endereço de e-mail. Se desativado, os usuários sem endereço de e-mail não vão poder usar os recursos que precisam disso."
|
||||
"require_user_email_description": "Obriga que os usuários tenham um endereço de e-mail. Se desativado, os usuários sem endereço de e-mail não vão poder usar os recursos que precisam disso.",
|
||||
"view": "Ver",
|
||||
"toggle_columns": "Alternar colunas",
|
||||
"locale": "Localização",
|
||||
"ldap_id": "ID LDAP",
|
||||
"reauthentication": "Re-autenticação",
|
||||
"clear_filters": "Limpar filtros"
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
"docs": "Документация",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"remove_custom_claim": "Удалить пользовательский claim",
|
||||
"add_custom_claim": "Добавить пользовательский claim",
|
||||
"remove_custom_claim": "Удалить пользовательское утверждение",
|
||||
"add_custom_claim": "Добавить пользовательское утверждение",
|
||||
"add_another": "Добавить ещё",
|
||||
"select_a_date": "Выбрать дату",
|
||||
"select_file": "Выбрать файл",
|
||||
"profile_picture": "Изображение профиля",
|
||||
"profile_picture_is_managed_by_ldap_server": "Изображение профиля управляется LDAP сервером и не может быть изменено здесь.",
|
||||
"profile_picture_is_managed_by_ldap_server": "Изображение профиля управляется сервером LDAP и не может быть изменено здесь.",
|
||||
"click_profile_picture_to_upload_custom": "Нажмите на изображение профиля, чтобы загрузить его из ваших файлов.",
|
||||
"image_should_be_in_format": "Изображение должно быть в формате PNG или JPEG.",
|
||||
"items_per_page": "Элементов на странице",
|
||||
"no_items_found": "Элементов не найдено",
|
||||
"no_items_found": "Элементы не найдены",
|
||||
"select_items": "Выбрать элементы...",
|
||||
"search": "Поиск...",
|
||||
"expand_card": "Развернуть карточку",
|
||||
@@ -41,21 +41,21 @@
|
||||
"this_browser_does_not_support_passkeys": "Этот браузер не поддерживает пасскеи. Пожалуйста, воспользуйтесь альтернативным способом входа.",
|
||||
"an_unknown_error_occurred": "Произошла неизвестная ошибка",
|
||||
"authentication_process_was_aborted": "Процесс аутентификации был прерван",
|
||||
"error_occurred_with_authenticator": "С аутентификатором произошла ошибка",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Аутентификатор не поддерживает discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "Аутентификатор не поддерживает resident keys",
|
||||
"error_occurred_with_authenticator": "Произошла ошибка аутентификатора",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Аутентификатор не поддерживает обнаруживаемые учетные данные",
|
||||
"authenticator_does_not_support_resident_keys": "Аутентификатор не поддерживает резидентные ключи",
|
||||
"passkey_was_previously_registered": "Этот пасскей был ранее зарегистрирован",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Аутентификатор не поддерживает ни один из запрошенных алгоритмов",
|
||||
"authenticator_timed_out": "Время ожидания аутентификатора истекло",
|
||||
"critical_error_occurred_contact_administrator": "Произошла критическая ошибка. Обратитесь к администратору.",
|
||||
"sign_in_to": "Вход в {name}",
|
||||
"sign_in_to": "Войти в {name}",
|
||||
"client_not_found": "Клиент не найден",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> запрашивает доступ к следующей информации:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Вы хотите войти в <b>{client}</b> с помощью вашей учетной записи {appName}?",
|
||||
"email": "Электронная почта",
|
||||
"view_your_email_address": "Просмотр адреса электронной почты",
|
||||
"profile": "Профиль",
|
||||
"view_your_profile_information": "Просмотр информации о вашем профиле",
|
||||
"view_your_profile_information": "Просмотр информации вашего профиля",
|
||||
"groups": "Группы",
|
||||
"view_the_groups_you_are_a_member_of": "Просмотр групп, в которых вы состоите",
|
||||
"cancel": "Отменить",
|
||||
@@ -64,7 +64,7 @@
|
||||
"client_logo": "Логотип клиента",
|
||||
"sign_out": "Выйти",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Вы хотите выйти из {appName} с учетной записью <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Вход в {appName}",
|
||||
"sign_in_to_appname": "Войти в {appName}",
|
||||
"please_try_to_sign_in_again": "Пожалуйста, попробуйте войти снова.",
|
||||
"authenticate_with_passkey_to_access_account": "Авторизуйтесь с использованием пасскея для доступа к вашей учетной записи.",
|
||||
"authenticate": "Авторизоваться",
|
||||
@@ -74,8 +74,8 @@
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Если у вас нет доступа к вашему пасскею, вы можете войти одним из следующих способов.",
|
||||
"use_your_passkey_instead": "Воспользоваться пасскеем вместо этого?",
|
||||
"email_login": "Вход через электронную почту",
|
||||
"enter_a_login_code_to_sign_in": "Введите предварительно созданный код входа.",
|
||||
"sign_in_with_login_code": "Войти с кодом для входа",
|
||||
"enter_a_login_code_to_sign_in": "Введите код входа, чтобы войти.",
|
||||
"sign_in_with_login_code": "Войти с помощью кода входа",
|
||||
"request_a_login_code_via_email": "Запросить код входа на электронную почту.",
|
||||
"go_back": "Назад",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Письмо было отправлено на указанный адрес электронной почты, если он существует в системе.",
|
||||
@@ -83,23 +83,23 @@
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Введите ваш адрес электронной почты, чтобы получить письмо с кодом входа.",
|
||||
"your_email": "Ваш адрес электронной почты",
|
||||
"submit": "Отправить",
|
||||
"enter_the_code_you_received_to_sign_in": "Введите полученный код входа.",
|
||||
"enter_the_code_you_received_to_sign_in": "Введите полученный код, чтобы войти.",
|
||||
"code": "Код",
|
||||
"invalid_redirect_url": "Неправильный URL-адрес перенаправления",
|
||||
"audit_log": "Журнал аудита",
|
||||
"users": "Пользователи",
|
||||
"user_groups": "Группы пользователей",
|
||||
"oidc_clients": "Клиенты OIDC",
|
||||
"api_keys": "API ключи",
|
||||
"api_keys": "Ключи API",
|
||||
"application_configuration": "Конфигурация приложения",
|
||||
"settings": "Настройки",
|
||||
"update_pocket_id": "Обновите Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "Смотрите активность вашей учетной записи за последние 3 месяца.",
|
||||
"powered_by": "Работает на",
|
||||
"see_your_account_activities_from_the_last_3_months": "Смотрите активность своей учетной записи за последние 3 месяца.",
|
||||
"time": "Время",
|
||||
"event": "Событие",
|
||||
"approximate_location": "Приблизительное местоположение",
|
||||
"ip_address": "IP адрес",
|
||||
"approximate_location": "Примерное местоположение",
|
||||
"ip_address": "IP-адрес",
|
||||
"device": "Устройство",
|
||||
"client": "Клиент",
|
||||
"unknown": "Неизвестно",
|
||||
@@ -120,7 +120,7 @@
|
||||
"last_name": "Фамилия",
|
||||
"username": "Имя пользователя",
|
||||
"save": "Сохранить",
|
||||
"username_can_only_contain": "Имя пользователя может содержать только строчные буквы, цифры, знак подчеркивания, точки, дефиса и символ '@'",
|
||||
"username_can_only_contain": "Имя пользователя может содержать только строчные буквы, цифры, подчеркивания, точки, дефисы и символ '@'",
|
||||
"username_must_start_with": "Имя пользователя должно начинаться с буквы или цифры",
|
||||
"username_must_end_with": "Имя пользователя должно заканчиваться буквой или цифрой",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Войдите, используя следующий код. Код истечет через 15 минут.",
|
||||
@@ -134,29 +134,29 @@
|
||||
"passkey_name_updated_successfully": "Имя пасскея успешно обновлено",
|
||||
"name_passkey": "Имя пасскея",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Назовите ваш пасскей, чтобы легко идентифицировать его позже.",
|
||||
"create_api_key": "Создать API ключ",
|
||||
"add_a_new_api_key_for_programmatic_access": "Добавь новый ключ API для программного доступа к <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>.",
|
||||
"add_api_key": "Добавить API ключ",
|
||||
"manage_api_keys": "Управление API ключами",
|
||||
"api_key_created": "API ключ создан",
|
||||
"create_api_key": "Создать ключ API",
|
||||
"add_a_new_api_key_for_programmatic_access": "Добавить новый ключ API для программного доступа к <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>.",
|
||||
"add_api_key": "Добавить ключ API",
|
||||
"manage_api_keys": "Управление ключами API",
|
||||
"api_key_created": "Ключ API создан",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "По соображениям безопасности, этот ключ будет показан только один раз. Пожалуйста, храните его в безопасном месте.",
|
||||
"description": "Описание",
|
||||
"api_key": "API ключ",
|
||||
"api_key": "Ключ API",
|
||||
"close": "Закрыть",
|
||||
"name_to_identify_this_api_key": "Имя для идентификации API ключа.",
|
||||
"name_to_identify_this_api_key": "Имя для идентификации ключа API.",
|
||||
"expires_at": "Действителен до",
|
||||
"when_this_api_key_will_expire": "Когда срок действия этого API ключа истечет.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Опциональное описание, чтобы помочь определить цель этого ключа.",
|
||||
"expiration_date_must_be_in_the_future": "Дата истечения должна быть определена в будущем",
|
||||
"revoke_api_key": "Отозвать API ключ",
|
||||
"when_this_api_key_will_expire": "Когда истечет срок действия этого ключа API.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Необязательное описание, которое поможет определить назначение этого ключа.",
|
||||
"expiration_date_must_be_in_the_future": "Дата истечения срока действия должна быть в будущем",
|
||||
"revoke_api_key": "Отозвать ключ API",
|
||||
"never": "Никогда",
|
||||
"revoke": "Отозвать",
|
||||
"api_key_revoked_successfully": "API ключ успешно отозван",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Вы уверены, что хотите отозвать API ключ \"{apiKeyName}\"? Это разрушит интеграцию, использующую этот ключ.",
|
||||
"api_key_revoked_successfully": "Ключ API успешно отозван",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Вы уверены, что хотите отозвать ключ API \"{apiKeyName}\"? Любые интеграции, использующие этот ключ, перестанут работать.",
|
||||
"last_used": "Последнее использование",
|
||||
"actions": "Действия",
|
||||
"images_updated_successfully": "Изображения успешно обновлены",
|
||||
"general": "Основное",
|
||||
"general": "Общее",
|
||||
"configure_smtp_to_send_emails": "Включить уведомления пользователей по электронной почте при обнаружении логина с нового устройства или локации.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.",
|
||||
@@ -274,10 +274,10 @@
|
||||
"one_time_link": "Одноразовая ссылка",
|
||||
"use_this_link_to_sign_in_once": "Используйте эту ссылку, чтобы войти единожды. Это необходимо для пользователей, которые ещё не добавили пасскей или потеряли его.",
|
||||
"add": "Добавить",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"callback_urls": "URL-адреса обратного вызова",
|
||||
"logout_callback_urls": "URL-адреса обратного вызова при выходе",
|
||||
"public_client": "Публичный клиент",
|
||||
"public_clients_description": "Публичные клиенты не имеют клиентского секрета. Они предназначены для мобильных, SPA и нативных приложений, где секретные данные нельзя надежно хранить.",
|
||||
"public_clients_description": "Публичные клиенты не имеют секрета клиента. Они предназначены для мобильных, SPA и нативных приложений, где секреты нельзя надежно хранить.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
|
||||
"requires_reauthentication": "Требуется повторная аутентификация",
|
||||
@@ -286,31 +286,31 @@
|
||||
"change_logo": "Изменить логотип",
|
||||
"upload_logo": "Загрузить логотип",
|
||||
"remove_logo": "Удалить логотип",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Вы уверены, что хотите удалить этот OIDC клиент?",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Вы уверены, что хотите удалить этого клиента OIDC?",
|
||||
"oidc_client_deleted_successfully": "Клиент OIDC успешно удален",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Включен",
|
||||
"authorization_url": "URL-адрес авторизации",
|
||||
"oidc_discovery_url": "URL-адрес обнаружения OIDC",
|
||||
"token_url": "URL-адрес токена",
|
||||
"userinfo_url": "URL-адрес информации о пользователе",
|
||||
"logout_url": "URL-адрес выхода",
|
||||
"certificate_url": "URL-адрес сертификата",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"oidc_client_updated_successfully": "OIDC клиент успешно обновлен",
|
||||
"create_new_client_secret": "Создать новый клиентский секрет",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Вы уверены, что хотите создать новый клиентский секрет? Старый будет аннулирован.",
|
||||
"oidc_client_updated_successfully": "Клиент OIDC успешно обновлен",
|
||||
"create_new_client_secret": "Создать новый секрет клиента",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Вы уверены, что хотите создать новый секрет клиента? Старый будет аннулирован.",
|
||||
"generate": "Сгенерировать",
|
||||
"new_client_secret_created_successfully": "Новый клиентский секрет успешно сгенерирован",
|
||||
"new_client_secret_created_successfully": "Новый секрет клиента успешно сгенерирован",
|
||||
"allowed_user_groups_updated_successfully": "Разрешенные группы пользователей успешно обновлены",
|
||||
"oidc_client_name": "OIDC клиент {name}",
|
||||
"client_id": "ID клиента",
|
||||
"client_secret": "Клиентский секрет",
|
||||
"oidc_client_name": "Клиент OIDC {name}",
|
||||
"client_id": "Идентификатор клиента",
|
||||
"client_secret": "Секрет клиента",
|
||||
"show_more_details": "Показать больше деталей",
|
||||
"allowed_user_groups": "Разрешенные группы пользователей",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Добавить группы пользователей к этому клиенту для ограничения доступа пользователей в этих группах. Если группы пользователей не выбраны, все пользователи будут иметь доступ к этому клиенту.",
|
||||
"favicon": "Иконка",
|
||||
"light_mode_logo": "Логотип для светлой темы",
|
||||
"dark_mode_logo": "Логотип для темной темы",
|
||||
"favicon": "Значок",
|
||||
"light_mode_logo": "Логотип светлого режима",
|
||||
"dark_mode_logo": "Логотип темного режима",
|
||||
"background_image": "Фоновое изображение",
|
||||
"language": "Язык",
|
||||
"reset_profile_picture_question": "Сбросить изображение профиля?",
|
||||
@@ -318,63 +318,63 @@
|
||||
"reset": "Сбросить",
|
||||
"reset_to_default": "Сбросить по умолчанию",
|
||||
"profile_picture_has_been_reset": "Изображение профиля было сброшено. Обновление может занять несколько минут.",
|
||||
"select_the_language_you_want_to_use": "Выберите язык, который вы хотите использовать. Обратите внимание на то, что часть текста может быть переведена автоматически и содержать неточности.",
|
||||
"select_the_language_you_want_to_use": "Выберите язык, который вы хотите использовать. Обратите внимание, что часть текста может быть переведена автоматически и содержать неточности.",
|
||||
"contribute_to_translation": "Если вы нашли ошибку, приглашаем вас помочь с переводом на <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Персональный",
|
||||
"personal": "Личный",
|
||||
"global": "Глобальный",
|
||||
"all_users": "Все пользователи",
|
||||
"all_events": "Все события",
|
||||
"all_clients": "Все клиенты",
|
||||
"all_locations": "Все местоположения",
|
||||
"global_audit_log": "Глобальный журнал аудита",
|
||||
"see_all_account_activities_from_the_last_3_months": "Смотрите всю активность пользователей за последние 3 месяца.",
|
||||
"see_all_account_activities_from_the_last_3_months": "Смотрите активность всех пользователей за последние 3 месяца.",
|
||||
"token_sign_in": "Вход с помощью токена",
|
||||
"client_authorization": "Авторизация в клиенте",
|
||||
"new_client_authorization": "Новая авторизация в клиенте",
|
||||
"client_authorization": "Авторизация клиента",
|
||||
"new_client_authorization": "Авторизация нового клиента",
|
||||
"disable_animations": "Отключить анимации",
|
||||
"turn_off_ui_animations": "Выключить анимации по всему интерфейсу.",
|
||||
"turn_off_ui_animations": "Отключить все анимации в интерфейсе.",
|
||||
"user_disabled": "Учетная запись отключена",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Отключенные пользователи не могут войти или использовать сервисы.",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Отключенные пользователи не могут входить или использовать сервисы.",
|
||||
"user_disabled_successfully": "Пользователь успешно отключен.",
|
||||
"user_enabled_successfully": "Пользователь успешно включен.",
|
||||
"status": "Статус",
|
||||
"disable_firstname_lastname": "Отключить {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Вы уверены, что хотите отключить этого пользователя? Они не смогут войти в систему или получить доступ к любым сервисам.",
|
||||
"ldap_soft_delete_users": "Оставить отключенных пользователей от LDAP.",
|
||||
"ldap_soft_delete_users_description": "Когда включено, пользователи удалённые из LDAP будут отключены вместо удаления из системы.",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Вы уверены, что хотите отключить этого пользователя? Он не сможет войти или получить доступ к каким-либо сервисам.",
|
||||
"ldap_soft_delete_users": "Сохранять отключенных пользователей из LDAP.",
|
||||
"ldap_soft_delete_users_description": "Если включено, пользователи, удаленные из LDAP, будут отключены, а не удалены из системы.",
|
||||
"login_code_email_success": "Код входа был отправлен пользователю.",
|
||||
"send_email": "Отправить письмо",
|
||||
"show_code": "Показать код",
|
||||
"callback_url_description": "URL-адрес(а) предоставленные вашим клиентом. Будет автоматически добавлен если оставить пустым. Маски (*) поддерживаются, но лучше избегайте их для повышения безопасности.",
|
||||
"logout_callback_url_description": "URL-адрес(а), предоставленный вашим клиентом для выхода. Маски (*) поддерживаются, но лучше избегайте их для повышения безопасности.",
|
||||
"api_key_expiration": "Истечение срока действия API ключа",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда истечет срок действия API ключа.",
|
||||
"callback_url_description": "URL-адреса, предоставленные клиентом. Будет автоматически добавлен, если оставить пустым. Подстановочные знаки (*) поддерживаются, но их лучше избегать для большей безопасности.",
|
||||
"logout_callback_url_description": "URL-адреса, предоставленные клиентом. Подстановочные знаки (*) поддерживаются, но их лучше избегать для большей безопасности.",
|
||||
"api_key_expiration": "Истечение срока действия ключа API",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда срок действия его ключа API истекает.",
|
||||
"authorize_device": "Авторизовать устройство",
|
||||
"the_device_has_been_authorized": "Устройство авторизовано.",
|
||||
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
|
||||
"authorize": "Авторизовать",
|
||||
"federated_client_credentials": "Федеративные учетные данные клиента",
|
||||
"federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете авторизовывать OIDC клиентов, используя JWT токены, выпущенные третьими сторонами.",
|
||||
"federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете аутентифицировать клиентов OIDC с помощью токенов JWT, выпущенных сторонними поставщиками удостоверений.",
|
||||
"add_federated_client_credential": "Добавить федеративные учетные данные клиента",
|
||||
"add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента",
|
||||
"oidc_allowed_group_count": "Кол-во разрешенных групп",
|
||||
"unrestricted": "Не ограничено",
|
||||
"show_advanced_options": "Показать дополнительные опции",
|
||||
"hide_advanced_options": "Скрыть дополнительные опции",
|
||||
"oidc_data_preview": "Предпросмотр данных OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Предпросмотр данных OIDC, которые будут отправлены разным пользователям",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "Содержимое ID Token",
|
||||
"access_token_payload": "Содержимое Access Token",
|
||||
"userinfo_endpoint_response": "Ответ Userinfo эндпоинта",
|
||||
"oidc_allowed_group_count": "Число разрешенных групп",
|
||||
"unrestricted": "Неограниченно",
|
||||
"show_advanced_options": "Показать дополнительные параметры",
|
||||
"hide_advanced_options": "Скрыть дополнительные параметры",
|
||||
"oidc_data_preview": "Предварительный просмотр данных OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Предварительный просмотр данных OIDC, которые будут отправлены для различных пользователей",
|
||||
"id_token": "Токен идентификации",
|
||||
"access_token": "Токен доступа",
|
||||
"userinfo": "Информация о пользователе",
|
||||
"id_token_payload": "Полезная нагрузка токена идентификации",
|
||||
"access_token_payload": "Полезная нагрузка токена доступа",
|
||||
"userinfo_endpoint_response": "Ответ конечной точки информации о пользователе",
|
||||
"copy": "Копировать",
|
||||
"no_preview_data_available": "Предварительный просмотр данных не доступен",
|
||||
"no_preview_data_available": "Нет данных предварительного просмотра",
|
||||
"copy_all": "Копировать все",
|
||||
"preview": "Предпросмотр",
|
||||
"preview": "Предварительный просмотр",
|
||||
"preview_for_user": "Предпросмотр для {name}",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Предпросмотр данных OIDC, которые будут отправлены для этого пользователя",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Предварительный просмотр данных OIDC, которые будут отправлены для этого пользователя",
|
||||
"show": "Показать",
|
||||
"select_an_option": "Выберите опцию",
|
||||
"select_user": "Выбрать пользователя",
|
||||
@@ -442,18 +442,24 @@
|
||||
"revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.",
|
||||
"revoke_access_successful": "Доступ к {clientName} успешно отозван.",
|
||||
"last_signed_in_ago": "Последний вход {time} назад",
|
||||
"invalid_client_id": "ID клиента может содержать только буквы, цифры, подчеркивания и дефисы",
|
||||
"custom_client_id_description": "Установите пользовательский ID клиента, если это нужно для вашего приложения. Если нет, оставьте поле пустым, чтобы он был сгенерирован случайным образом.",
|
||||
"invalid_client_id": "Идентификатор клиента может содержать только буквы, цифры, подчеркивания и дефисы",
|
||||
"custom_client_id_description": "Укажите собственный идентификатор клиента, если это требуется для вашего приложения. В противном случае оставьте поле пустым, чтобы сгенерировать случайный идентификатор.",
|
||||
"generated": "Сгенерированный",
|
||||
"administration": "Администрирование",
|
||||
"group_rdn_attribute_description": "Атрибут, который используется в различающемся имени группы (DN).",
|
||||
"group_rdn_attribute_description": "Атрибут, который используется в отличительном имени (DN) группы.",
|
||||
"display_name_attribute": "Атрибут отображаемого имени",
|
||||
"display_name": "Отображаемое имя",
|
||||
"configure_application_images": "Настройка изображений приложения",
|
||||
"ui_config_disabled_info_title": "Конфигурация пользовательского интерфейса отключена",
|
||||
"ui_config_disabled_info_description": "Конфигурация пользовательского интерфейса отключена, потому что настройки приложения управляются через переменные среды. Некоторые настройки могут быть недоступны для редактирования.",
|
||||
"logo_from_url_description": "Вставьте прямой URL-адрес изображения (svg, png, webp). Ищите иконки на <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> или <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Недопустимый URL",
|
||||
"require_user_email": "Требовать адрес электронной почты",
|
||||
"require_user_email_description": "Требует, чтобы у пользователей был адрес электронной почты. Если эта функция отключена, пользователи без адреса электронной почты не смогут пользоваться функциями, для которых нужен адрес электронной почты."
|
||||
"logo_from_url_description": "Укажите прямой URL-адрес изображения (svg, png, webp). Значки можно найти на <link href=\\\"https://selfh.st/icons\\\">Selfh.st Icons</link> или <link href=\\\"https://dashboardicons.com\\\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Неверный URL-адрес",
|
||||
"require_user_email": "Обязательный адрес электронной почты",
|
||||
"require_user_email_description": "Требует наличия адреса электронной почты у пользователей. Если отключено, пользователи без адреса электронной почты не смогут использовать функции, требующие адреса электронной почты.",
|
||||
"view": "Вид",
|
||||
"toggle_columns": "Показывать столбцы",
|
||||
"locale": "Язык",
|
||||
"ldap_id": "LDAP ID",
|
||||
"reauthentication": "Повторная аутентификация",
|
||||
"clear_filters": "Сбросить фильтры"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Klistra in en direkt bild-URL (svg, png, webp). Hitta ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Ogiltig URL",
|
||||
"require_user_email": "Kräver e-postadress",
|
||||
"require_user_email_description": "Kräver att användarna har en e-postadress. Om funktionen är inaktiverad kan användare utan e-postadress inte använda funktioner som kräver en e-postadress."
|
||||
"require_user_email_description": "Kräver att användarna har en e-postadress. Om funktionen är inaktiverad kan användare utan e-postadress inte använda funktioner som kräver en e-postadress.",
|
||||
"view": "Visa",
|
||||
"toggle_columns": "Växla kolumner",
|
||||
"locale": "Lokalisering",
|
||||
"ldap_id": "LDAP-ID",
|
||||
"reauthentication": "Omverifiering",
|
||||
"clear_filters": "Rensa filter"
|
||||
}
|
||||
|
||||
465
frontend/messages/tr.json
Normal file
465
frontend/messages/tr.json
Normal file
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "My Account",
|
||||
"logout": "Logout",
|
||||
"confirm": "Confirm",
|
||||
"docs": "Docs",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
"add_custom_claim": "Add custom claim",
|
||||
"add_another": "Add another",
|
||||
"select_a_date": "Select a date",
|
||||
"select_file": "Select File",
|
||||
"profile_picture": "Profile Picture",
|
||||
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.",
|
||||
"click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.",
|
||||
"image_should_be_in_format": "The image should be in PNG or JPEG format.",
|
||||
"items_per_page": "Items per page",
|
||||
"no_items_found": "No items found",
|
||||
"select_items": "Select items...",
|
||||
"search": "Search...",
|
||||
"expand_card": "Expand card",
|
||||
"copied": "Copied",
|
||||
"click_to_copy": "Click to copy",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"go_back_to_home": "Go back to home",
|
||||
"alternative_sign_in_methods": "Alternative Sign In Methods",
|
||||
"login_background": "Login background",
|
||||
"logo": "Logo",
|
||||
"login_code": "Login Code",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.",
|
||||
"one_hour": "1 hour",
|
||||
"twelve_hours": "12 hours",
|
||||
"one_day": "1 day",
|
||||
"one_week": "1 week",
|
||||
"one_month": "1 month",
|
||||
"expiration": "Expiration",
|
||||
"generate_code": "Generate Code",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser unsupported",
|
||||
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please use an alternative sign in method.",
|
||||
"an_unknown_error_occurred": "An unknown error occurred",
|
||||
"authentication_process_was_aborted": "The authentication process was aborted",
|
||||
"error_occurred_with_authenticator": "An error occurred with the authenticator",
|
||||
"authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys",
|
||||
"passkey_was_previously_registered": "This passkey was previously registered",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms",
|
||||
"authenticator_timed_out": "The authenticator timed out",
|
||||
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.",
|
||||
"sign_in_to": "Sign in to {name}",
|
||||
"client_not_found": "Client not found",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to <b>{client}</b> with your {appName} account?",
|
||||
"email": "Email",
|
||||
"view_your_email_address": "View your email address",
|
||||
"profile": "Profile",
|
||||
"view_your_profile_information": "View your profile information",
|
||||
"groups": "Groups",
|
||||
"view_the_groups_you_are_a_member_of": "View the groups you are a member of",
|
||||
"cancel": "Cancel",
|
||||
"sign_in": "Sign in",
|
||||
"try_again": "Try again",
|
||||
"client_logo": "Client Logo",
|
||||
"sign_out": "Sign out",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of {appName} with the account <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Sign in to {appName}",
|
||||
"please_try_to_sign_in_again": "Please try to sign in again.",
|
||||
"authenticate_with_passkey_to_access_account": "Authenticate yourself with your passkey to access your account.",
|
||||
"authenticate": "Authenticate",
|
||||
"please_try_again": "Please try again.",
|
||||
"continue": "Continue",
|
||||
"alternative_sign_in": "Alternative Sign In",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you don't have access to your passkey, you can sign in using one of the following methods.",
|
||||
"use_your_passkey_instead": "Use your passkey instead?",
|
||||
"email_login": "Email Login",
|
||||
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
||||
"sign_in_with_login_code": "Sign in with login code",
|
||||
"request_a_login_code_via_email": "Request a login code via email.",
|
||||
"go_back": "Go back",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.",
|
||||
"enter_code": "Enter code",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.",
|
||||
"your_email": "Your email",
|
||||
"submit": "Submit",
|
||||
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Invalid redirect URL",
|
||||
"audit_log": "Audit Log",
|
||||
"users": "Users",
|
||||
"user_groups": "User Groups",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Application Configuration",
|
||||
"settings": "Settings",
|
||||
"update_pocket_id": "Update Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
|
||||
"time": "Time",
|
||||
"event": "Event",
|
||||
"approximate_location": "Approximate Location",
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"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.",
|
||||
"account_settings": "Account Settings",
|
||||
"passkey_missing": "Passkey missing",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.",
|
||||
"single_passkey_configured": "Single Passkey Configured",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.",
|
||||
"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.",
|
||||
"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",
|
||||
"first_name": "First name",
|
||||
"last_name": "Last name",
|
||||
"username": "Username",
|
||||
"save": "Save",
|
||||
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
|
||||
"username_must_start_with": "Username must start with an alphanumeric character",
|
||||
"username_must_end_with": "Username must end with an alphanumeric character",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.",
|
||||
"or_visit": "or visit",
|
||||
"added_on": "Added on",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?",
|
||||
"passkey_deleted_successfully": "Passkey deleted successfully",
|
||||
"delete_passkey_name": "Delete {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey name updated successfully",
|
||||
"name_passkey": "Name Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.",
|
||||
"create_api_key": "Create API Key",
|
||||
"add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access to the <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>.",
|
||||
"add_api_key": "Add API Key",
|
||||
"manage_api_keys": "Manage API Keys",
|
||||
"api_key_created": "API Key Created",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.",
|
||||
"description": "Description",
|
||||
"api_key": "API Key",
|
||||
"close": "Close",
|
||||
"name_to_identify_this_api_key": "Name to identify this API key.",
|
||||
"expires_at": "Expires At",
|
||||
"when_this_api_key_will_expire": "When this API key will expire.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.",
|
||||
"expiration_date_must_be_in_the_future": "Expiration date must be in the future",
|
||||
"revoke_api_key": "Revoke API Key",
|
||||
"never": "Never",
|
||||
"revoke": "Revoke",
|
||||
"api_key_revoked_successfully": "API key revoked successfully",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.",
|
||||
"last_used": "Last Used",
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
"update": "Update",
|
||||
"email_configuration_updated_successfully": "Email configuration updated successfully",
|
||||
"save_changes_question": "Save changes?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?",
|
||||
"save_and_send": "Save and send",
|
||||
"test_email_sent_successfully": "Test email sent successfully to your email address.",
|
||||
"failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.",
|
||||
"smtp_configuration": "SMTP Configuration",
|
||||
"smtp_host": "SMTP Host",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_user": "SMTP User",
|
||||
"smtp_password": "SMTP Password",
|
||||
"smtp_from": "SMTP From",
|
||||
"smtp_tls_option": "SMTP TLS Option",
|
||||
"email_tls_option": "Email TLS Option",
|
||||
"skip_certificate_verification": "Skip Certificate Verification",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
"session_duration": "Session Duration",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.",
|
||||
"enable_self_account_editing": "Enable Self-Account Editing",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.",
|
||||
"emails_verified": "Emails Verified",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Whether the user's email should be marked as verified for the OIDC clients.",
|
||||
"ldap_configuration_updated_successfully": "LDAP configuration updated successfully",
|
||||
"ldap_disabled_successfully": "LDAP disabled successfully",
|
||||
"ldap_sync_finished": "LDAP sync finished",
|
||||
"client_configuration": "Client Configuration",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "User Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.",
|
||||
"groups_search_filter": "Groups Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.",
|
||||
"attribute_mapping": "Attribute Mapping",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.",
|
||||
"username_attribute": "Username Attribute",
|
||||
"user_mail_attribute": "User Mail Attribute",
|
||||
"user_first_name_attribute": "User First Name Attribute",
|
||||
"user_last_name_attribute": "User Last Name Attribute",
|
||||
"user_profile_picture_attribute": "User Profile Picture Attribute",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.",
|
||||
"group_members_attribute": "Group Members Attribute",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.",
|
||||
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
|
||||
"group_rdn_attribute": "Group RDN Attribute (in DN)",
|
||||
"admin_group_name": "Admin Group Name",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.",
|
||||
"disable": "Disable",
|
||||
"sync_now": "Sync now",
|
||||
"enable": "Enable",
|
||||
"user_created_successfully": "User created successfully",
|
||||
"create_user": "Create User",
|
||||
"add_a_new_user_to_appname": "Add a new user to {appName}",
|
||||
"add_user": "Add User",
|
||||
"manage_users": "Manage Users",
|
||||
"admin_privileges": "Admin Privileges",
|
||||
"admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.",
|
||||
"delete_firstname_lastname": "Delete {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?",
|
||||
"user_deleted_successfully": "User deleted successfully",
|
||||
"role": "Role",
|
||||
"source": "Source",
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
"local": "Local",
|
||||
"toggle_menu": "Toggle menu",
|
||||
"edit": "Edit",
|
||||
"user_groups_updated_successfully": "User groups updated successfully",
|
||||
"user_updated_successfully": "User updated successfully",
|
||||
"custom_claims_updated_successfully": "Custom claims updated successfully",
|
||||
"back": "Back",
|
||||
"user_details_firstname_lastname": "User Details {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.",
|
||||
"custom_claims": "Custom Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.",
|
||||
"user_group_created_successfully": "User group created successfully",
|
||||
"create_user_group": "Create User Group",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.",
|
||||
"add_group": "Add Group",
|
||||
"manage_user_groups": "Manage User Groups",
|
||||
"friendly_name": "Friendly Name",
|
||||
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
|
||||
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||
"delete_name": "Delete {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?",
|
||||
"user_group_deleted_successfully": "User group deleted successfully",
|
||||
"user_count": "User Count",
|
||||
"user_group_updated_successfully": "User group updated successfully",
|
||||
"users_updated_successfully": "Users updated successfully",
|
||||
"user_group_details_name": "User Group Details {name}",
|
||||
"assign_users_to_this_group": "Assign users to this group.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.",
|
||||
"oidc_client_created_successfully": "OIDC client created successfully",
|
||||
"create_oidc_client": "Create OIDC Client",
|
||||
"add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.",
|
||||
"add_oidc_client": "Add OIDC Client",
|
||||
"manage_oidc_clients": "Manage OIDC Clients",
|
||||
"one_time_link": "One Time Link",
|
||||
"use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or have lost it.",
|
||||
"add": "Add",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"requires_reauthentication": "Requires Re-Authentication",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Change Logo",
|
||||
"upload_logo": "Upload Logo",
|
||||
"remove_logo": "Remove Logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?",
|
||||
"oidc_client_deleted_successfully": "OIDC client deleted successfully",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"oidc_client_updated_successfully": "OIDC client updated successfully",
|
||||
"create_new_client_secret": "Create new client secret",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
|
||||
"generate": "Generate",
|
||||
"new_client_secret_created_successfully": "New client secret created successfully",
|
||||
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
|
||||
"oidc_client_name": "OIDC Client {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Show more details",
|
||||
"allowed_user_groups": "Allowed User Groups",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Light Mode Logo",
|
||||
"dark_mode_logo": "Dark Mode Logo",
|
||||
"background_image": "Background Image",
|
||||
"language": "Language",
|
||||
"reset_profile_picture_question": "Reset profile picture?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image and reset the profile picture to default. Do you want to continue?",
|
||||
"reset": "Reset",
|
||||
"reset_to_default": "Reset to default",
|
||||
"profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.",
|
||||
"select_the_language_you_want_to_use": "Select the language you want to use. Please note that some text may be automatically translated and could be inaccurate.",
|
||||
"contribute_to_translation": "If you find an issue you're welcome to contribute to the translation on <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Personal",
|
||||
"global": "Global",
|
||||
"all_users": "All Users",
|
||||
"all_events": "All Events",
|
||||
"all_clients": "All Clients",
|
||||
"all_locations": "All Locations",
|
||||
"global_audit_log": "Global Audit Log",
|
||||
"see_all_account_activities_from_the_last_3_months": "See all user activity for the last 3 months.",
|
||||
"token_sign_in": "Token Sign In",
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
"user_enabled_successfully": "User has been enabled successfully.",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Will be automatically added if left blank. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"logout_callback_url_description": "URL(s) provided by your client for logout. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
|
||||
"authorize_device": "Authorize Device",
|
||||
"the_device_has_been_authorized": "The device has been authorized.",
|
||||
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
|
||||
"authorize": "Authorize",
|
||||
"federated_client_credentials": "Federated Client Credentials",
|
||||
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
|
||||
"add_federated_client_credential": "Add Federated Client Credential",
|
||||
"add_another_federated_client_credential": "Add another federated client credential",
|
||||
"oidc_allowed_group_count": "Allowed Group Count",
|
||||
"unrestricted": "Unrestricted",
|
||||
"show_advanced_options": "Show Advanced Options",
|
||||
"hide_advanced_options": "Hide Advanced Options",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name}",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply",
|
||||
"signup_token": "Signup Token",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "Create a signup token to allow new user registration.",
|
||||
"usage_limit": "Usage Limit",
|
||||
"number_of_times_token_can_be_used": "Number of times the signup token can be used.",
|
||||
"expires": "Expires",
|
||||
"signup": "Sign Up",
|
||||
"user_creation": "User Creation",
|
||||
"configure_user_creation": "Manage user creation settings, including signup methods and default permissions for new users.",
|
||||
"user_creation_groups_description": "Assign these groups automatically to new users upon signup.",
|
||||
"user_creation_claims_description": "Assign these custom claims automatically to new users upon signup.",
|
||||
"user_creation_updated_successfully": "User creation settings updated successfully.",
|
||||
"signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.",
|
||||
"signup_requires_valid_token": "A valid signup token is required to create an account",
|
||||
"validating_signup_token": "Validating signup token",
|
||||
"go_to_login": "Go to login",
|
||||
"signup_to_appname": "Sign Up to {appName}",
|
||||
"create_your_account_to_get_started": "Create your account to get started.",
|
||||
"initial_account_creation_description": "Please create your account to get started. You will be able to set up a passkey later.",
|
||||
"setup_your_passkey": "Set up your passkey",
|
||||
"create_a_passkey_to_securely_access_your_account": "Create a passkey to securely access your account. This will be your primary way to sign in.",
|
||||
"skip_for_now": "Skip for now",
|
||||
"account_created": "Account Created",
|
||||
"enable_user_signups": "Enable User Signups",
|
||||
"enable_user_signups_description": "Decide how users can sign up for new accounts in Pocket ID.",
|
||||
"user_signups_are_disabled": "User signups are currently disabled",
|
||||
"create_signup_token": "Create Signup Token",
|
||||
"view_active_signup_tokens": "View Active Signup Tokens",
|
||||
"manage_signup_tokens": "Manage Signup Tokens",
|
||||
"view_and_manage_active_signup_tokens": "View and manage active signup tokens.",
|
||||
"signup_token_deleted_successfully": "Signup token deleted successfully.",
|
||||
"expired": "Expired",
|
||||
"used_up": "Used Up",
|
||||
"active": "Active",
|
||||
"usage": "Usage",
|
||||
"created": "Created",
|
||||
"token": "Token",
|
||||
"loading": "Loading",
|
||||
"delete_signup_token": "Delete Signup Token",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Are you sure you want to delete this signup token? This action cannot be undone.",
|
||||
"signup_with_token": "Signup with token",
|
||||
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
|
||||
"signup_open": "Open Signup",
|
||||
"signup_open_description": "Anyone can create a new account without restrictions.",
|
||||
"of": "of",
|
||||
"skip_passkey_setup": "Skip Passkey Setup",
|
||||
"skip_passkey_setup_description": "It's highly recommended to set up a passkey because without one, you will be locked out of your account as soon as the session expires.",
|
||||
"my_apps": "My Apps",
|
||||
"no_apps_available": "No apps available",
|
||||
"contact_your_administrator_for_app_access": "Contact your administrator to get access to applications.",
|
||||
"launch": "Launch",
|
||||
"client_launch_url": "Client Launch URL",
|
||||
"client_launch_url_description": "The URL that will be opened when a user launches the app from the My Apps page.",
|
||||
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
|
||||
"revoke_access": "Revoke Access",
|
||||
"revoke_access_description": "Revoke access to <b>{clientName}</b>. <b>{clientName}</b> will no longer be able to access your account information.",
|
||||
"revoke_access_successful": "The access to {clientName} has been successfully revoked.",
|
||||
"last_signed_in_ago": "Last signed in {time} ago",
|
||||
"invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens",
|
||||
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||
"generated": "Generated",
|
||||
"administration": "Administration",
|
||||
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
|
||||
"display_name_attribute": "Display Name Attribute",
|
||||
"display_name": "Display Name",
|
||||
"configure_application_images": "Configure Application Images",
|
||||
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.",
|
||||
"logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> or <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Invalid URL",
|
||||
"require_user_email": "Require Email Address",
|
||||
"require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address.",
|
||||
"view": "View",
|
||||
"toggle_columns": "Toggle columns",
|
||||
"locale": "Locale",
|
||||
"ldap_id" : "LDAP ID",
|
||||
"reauthentication": "Re-authentication",
|
||||
"clear_filters" : "Clear Filters"
|
||||
}
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Вставте прямий URL-адресу зображення (svg, png, webp). Знайдіть іконки на <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> або <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "Недійсний URL-адреса",
|
||||
"require_user_email": "Потрібна адреса електронної пошти",
|
||||
"require_user_email_description": "Вимагає від користувачів наявність адреси електронної пошти. Якщо ця опція вимкнена, користувачі без адреси електронної пошти не зможуть користуватися функціями, для яких потрібна адреса електронної пошти."
|
||||
"require_user_email_description": "Вимагає від користувачів наявність адреси електронної пошти. Якщо ця опція вимкнена, користувачі без адреси електронної пошти не зможуть користуватися функціями, для яких потрібна адреса електронної пошти.",
|
||||
"view": "Перегляд",
|
||||
"toggle_columns": "Перемикання стовпців",
|
||||
"locale": "Локаль",
|
||||
"ldap_id": "LDAP-ідентифікатор",
|
||||
"reauthentication": "Повторна аутентифікація",
|
||||
"clear_filters": "Очистити фільтри"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "Dán URL hình ảnh trực tiếp (svg, png, webp). Tìm biểu tượng tại <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> hoặc <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||
"invalid_url": "URL không hợp lệ",
|
||||
"require_user_email": "Yêu cầu địa chỉ email",
|
||||
"require_user_email_description": "Yêu cầu người dùng phải có địa chỉ email. Nếu tính năng này bị vô hiệu hóa, những người dùng không có địa chỉ email sẽ không thể sử dụng các tính năng yêu cầu địa chỉ email."
|
||||
"require_user_email_description": "Yêu cầu người dùng phải có địa chỉ email. Nếu tính năng này bị vô hiệu hóa, những người dùng không có địa chỉ email sẽ không thể sử dụng các tính năng yêu cầu địa chỉ email.",
|
||||
"view": "Xem",
|
||||
"toggle_columns": "Bật/tắt cột",
|
||||
"locale": "Vùng",
|
||||
"ldap_id": "ID LDAP",
|
||||
"reauthentication": "Xác thực lại",
|
||||
"clear_filters": "Xóa bộ lọc"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "粘贴直接图片URL(svg、png、webp格式)。<link href=\"https://selfh.st/icons\">可在Selfh.st图标库</link>或<link href=\"https://dashboardicons.com\">仪表盘图标库</link>中查找图标。",
|
||||
"invalid_url": "无效网址",
|
||||
"require_user_email": "需要电子邮件地址",
|
||||
"require_user_email_description": "要求用户拥有电子邮件地址。若禁用此功能,没有电子邮件地址的用户将无法使用需要电子邮件地址的功能。"
|
||||
"require_user_email_description": "要求用户拥有电子邮件地址。若禁用此功能,没有电子邮件地址的用户将无法使用需要电子邮件地址的功能。",
|
||||
"view": "查看",
|
||||
"toggle_columns": "切换列",
|
||||
"locale": "区域设置",
|
||||
"ldap_id": "LDAP标识符",
|
||||
"reauthentication": "重新认证",
|
||||
"clear_filters": "清除筛选条件"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"logo_from_url_description": "貼上直接圖片網址(svg、png、webp)。在<link href=\"https://selfh.st/icons\">Selfh.st 圖示庫或</link> <link href=\"https://dashboardicons.com\">儀表板圖示庫中</link>尋找圖示。",
|
||||
"invalid_url": "無效網址",
|
||||
"require_user_email": "需要電子郵件地址",
|
||||
"require_user_email_description": "要求使用者必須擁有電子郵件地址。若此功能被停用,沒有電子郵件地址的使用者將無法使用需要電子郵件地址的功能。"
|
||||
"require_user_email_description": "要求使用者必須擁有電子郵件地址。若此功能被停用,沒有電子郵件地址的使用者將無法使用需要電子郵件地址的功能。",
|
||||
"view": "檢視",
|
||||
"toggle_columns": "切換欄位",
|
||||
"locale": "地區",
|
||||
"ldap_id": "LDAP 識別碼",
|
||||
"reauthentication": "重新驗證",
|
||||
"clear_filters": "清除篩選條件"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "1.13.1",
|
||||
"version": "1.14.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -14,50 +14,50 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^13.1.2",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"axios": "^1.12.0",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"jose": "^5.10.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"runed": "^0.31.1",
|
||||
"sveltekit-superforms": "^2.27.1",
|
||||
"sveltekit-superforms": "^2.28.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.0.9"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.2.0",
|
||||
"@inlang/paraglide-js": "^2.4.0",
|
||||
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
||||
"@inlang/plugin-message-format": "^4.0.0",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.525.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.36.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.47.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"bits-ui": "^2.8.11",
|
||||
"eslint": "^9.31.0",
|
||||
"@types/node": "^22.18.12",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"bits-ui": "^2.14.1",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.11.0",
|
||||
"eslint-plugin-svelte": "^3.12.5",
|
||||
"formsnap": "^2.0.1",
|
||||
"globals": "^16.3.0",
|
||||
"globals": "^16.4.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"rollup": "^4.46.3",
|
||||
"svelte": "^5.36.16",
|
||||
"svelte-check": "^4.3.0",
|
||||
"rollup": "^4.52.5",
|
||||
"svelte": "^5.41.3",
|
||||
"svelte-check": "^4.3.3",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"tslib": "^2.8.1",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.7"
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"sv",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
"zh-CN",
|
||||
|
||||
@@ -6,7 +6,20 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="manifest" href="/app.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="/img/static-logo-512.png">
|
||||
<link rel="apple-touch-icon" href="/img/static-logo-512.png" />
|
||||
<script>
|
||||
try {
|
||||
const mode = localStorage.getItem('mode-watcher-mode') || 'system';
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = mode === 'dark' || (mode === 'system' && prefersDark);
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
// Failing to set theme is not critical
|
||||
}
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
<script lang="ts" generics="T extends {id:string}">
|
||||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import * as Pagination from '$lib/components/ui/pagination';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import Empty from '$lib/icons/empty.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { debounced } from '$lib/utils/debounce-util';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { ChevronDown } from '@lucide/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Button from './ui/button/button.svelte';
|
||||
|
||||
let {
|
||||
items,
|
||||
requestOptions = $bindable(),
|
||||
selectedIds = $bindable(),
|
||||
withoutSearch = false,
|
||||
selectionDisabled = false,
|
||||
onRefresh,
|
||||
columns,
|
||||
rows
|
||||
}: {
|
||||
items: Paginated<T>;
|
||||
requestOptions: SearchPaginationSortRequest;
|
||||
selectedIds?: string[];
|
||||
withoutSearch?: boolean;
|
||||
selectionDisabled?: boolean;
|
||||
onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise<Paginated<T>>;
|
||||
columns: { label: string; hidden?: boolean; sortColumn?: string }[];
|
||||
rows: Snippet<[{ item: T }]>;
|
||||
} = $props();
|
||||
|
||||
let searchValue = $state('');
|
||||
let availablePageSizes: number[] = [20, 50, 100];
|
||||
|
||||
let allChecked = $derived.by(() => {
|
||||
if (!selectedIds || items.data.length === 0) return false;
|
||||
for (const item of items.data) {
|
||||
if (!selectedIds.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const onSearch = debounced(async (search: string) => {
|
||||
requestOptions.search = search;
|
||||
await onRefresh(requestOptions);
|
||||
searchValue = search;
|
||||
}, 300);
|
||||
|
||||
async function onAllCheck(checked: boolean) {
|
||||
const pageIds = items.data.map((item) => item.id);
|
||||
const current = selectedIds ?? [];
|
||||
|
||||
if (checked) {
|
||||
selectedIds = Array.from(new Set([...current, ...pageIds]));
|
||||
} else {
|
||||
selectedIds = current.filter((id) => !pageIds.includes(id));
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheck(checked: boolean, id: string) {
|
||||
const current = selectedIds ?? [];
|
||||
if (checked) {
|
||||
selectedIds = Array.from(new Set([...current, id]));
|
||||
} else {
|
||||
selectedIds = current.filter((selectedId) => selectedId !== id);
|
||||
}
|
||||
}
|
||||
|
||||
async function onPageChange(page: number) {
|
||||
requestOptions.pagination = { limit: items.pagination.itemsPerPage, page };
|
||||
onRefresh(requestOptions);
|
||||
}
|
||||
|
||||
async function onPageSizeChange(size: number) {
|
||||
requestOptions.pagination = { limit: size, page: 1 };
|
||||
onRefresh(requestOptions);
|
||||
}
|
||||
|
||||
async function onSort(column?: string, direction: 'asc' | 'desc' = 'asc') {
|
||||
if (!column) return;
|
||||
|
||||
requestOptions.sort = { column, direction };
|
||||
onRefresh(requestOptions);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !withoutSearch}
|
||||
<Input
|
||||
value={searchValue}
|
||||
class={cn(
|
||||
'relative z-50 mb-4 max-w-sm',
|
||||
items.data.length == 0 && searchValue == '' && 'hidden'
|
||||
)}
|
||||
placeholder={m.search()}
|
||||
type="text"
|
||||
oninput={(e: Event) => onSearch((e.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if items.data.length === 0 && searchValue === ''}
|
||||
<div class="my-5 flex flex-col items-center">
|
||||
<Empty class="text-muted-foreground h-20" />
|
||||
<p class="text-muted-foreground mt-3 text-sm">{m.no_items_found()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Table.Root class="min-w-full table-auto overflow-x-auto">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{#if selectedIds}
|
||||
<Table.Head class="w-12">
|
||||
<Checkbox
|
||||
disabled={selectionDisabled}
|
||||
checked={allChecked}
|
||||
onCheckedChange={(c: boolean) => onAllCheck(c as boolean)}
|
||||
/>
|
||||
</Table.Head>
|
||||
{/if}
|
||||
{#each columns as column}
|
||||
<Table.Head class={cn(column.hidden && 'sr-only', column.sortColumn && 'px-0')}>
|
||||
{#if column.sortColumn}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex items-center"
|
||||
onclick={() =>
|
||||
onSort(
|
||||
column.sortColumn,
|
||||
requestOptions.sort?.direction === 'desc' ? 'asc' : 'desc'
|
||||
)}
|
||||
>
|
||||
{column.label}
|
||||
{#if requestOptions.sort?.column === column.sortColumn}
|
||||
<ChevronDown
|
||||
class={cn(
|
||||
'ml-2 size-4',
|
||||
requestOptions.sort?.direction === 'asc' ? 'rotate-180' : ''
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
{column.label}
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each items.data as item}
|
||||
<Table.Row class={selectedIds?.includes(item.id) ? 'bg-muted/20' : ''}>
|
||||
{#if selectedIds}
|
||||
<Table.Cell class="w-12">
|
||||
<Checkbox
|
||||
disabled={selectionDisabled}
|
||||
checked={selectedIds.includes(item.id)}
|
||||
onCheckedChange={(c: boolean) => onCheck(c, item.id)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
{@render rows({ item })}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
|
||||
<div class="mt-5 flex flex-col-reverse items-center justify-between gap-3 sm:flex-row">
|
||||
<div class="flex items-center space-x-2">
|
||||
<p class="text-sm font-medium">{m.items_per_page()}</p>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={items.pagination.itemsPerPage.toString()}
|
||||
onValueChange={(v) => onPageSizeChange(Number(v))}
|
||||
>
|
||||
<Select.Trigger class="h-9 w-[80px]">
|
||||
{items.pagination.itemsPerPage}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each availablePageSizes as size}
|
||||
<Select.Item value={size.toString()}>{size}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Pagination.Root
|
||||
class="mx-0 w-auto"
|
||||
count={items.pagination.totalItems}
|
||||
perPage={items.pagination.itemsPerPage}
|
||||
{onPageChange}
|
||||
page={items.pagination.currentPage}
|
||||
>
|
||||
{#snippet children({ pages })}
|
||||
<Pagination.Content class="flex justify-end">
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type !== 'ellipsis' && page.value != 0}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={items.pagination.currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,69 +1,111 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import {translateAuditLogEvent} from "$lib/utils/audit-log-translator";
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { AuditLog } from '$lib/types/audit-log.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||
import type { AuditLog, AuditLogFilter } from '$lib/types/audit-log.type';
|
||||
import { translateAuditLogEvent } from '$lib/utils/audit-log-translator';
|
||||
|
||||
let {
|
||||
auditLogs,
|
||||
isAdmin = false,
|
||||
requestOptions
|
||||
filters
|
||||
}: {
|
||||
auditLogs: Paginated<AuditLog>;
|
||||
isAdmin?: boolean;
|
||||
requestOptions: SearchPaginationSortRequest;
|
||||
filters?: AuditLogFilter;
|
||||
} = $props();
|
||||
|
||||
const auditLogService = new AuditLogService();
|
||||
let tableRef: AdvancedTable<AuditLog>;
|
||||
|
||||
const columns: AdvancedTableColumn<AuditLog>[] = [
|
||||
{
|
||||
label: m.time(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
value: (item) => new Date(item.createdAt).toLocaleString()
|
||||
},
|
||||
{
|
||||
label: m.username(),
|
||||
column: 'username',
|
||||
hidden: !isAdmin,
|
||||
value: (item) => item.username ?? m.unknown()
|
||||
},
|
||||
{
|
||||
label: m.event(),
|
||||
column: 'event',
|
||||
sortable: true,
|
||||
cell: EventCell
|
||||
},
|
||||
{
|
||||
label: m.approximate_location(),
|
||||
key: 'location',
|
||||
value: (item) => formatLocation(item)
|
||||
},
|
||||
{
|
||||
label: m.ip_address(),
|
||||
column: 'ipAddress',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
label: m.device(),
|
||||
column: 'device',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
label: m.client(),
|
||||
key: 'client',
|
||||
value: (item) => item.data?.clientName
|
||||
}
|
||||
];
|
||||
|
||||
$effect(() => {
|
||||
if (filters) {
|
||||
tableRef?.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
export async function refresh() {
|
||||
await tableRef.refresh();
|
||||
}
|
||||
|
||||
function formatLocation(log: AuditLog) {
|
||||
if (log.city && log.country) {
|
||||
return `${log.city}, ${log.country}`;
|
||||
} else if (log.country) {
|
||||
return log.country;
|
||||
} else {
|
||||
return m.unknown();
|
||||
}
|
||||
}
|
||||
|
||||
function wrapFilters(filters?: Record<string, string>) {
|
||||
if (!filters) return undefined;
|
||||
return Object.fromEntries(
|
||||
Object.entries(filters)
|
||||
.filter(([_, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => [key, [value]])
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet EventCell({ item }: { item: AuditLog })}
|
||||
<Badge class="rounded-full" variant="outline">
|
||||
{translateAuditLogEvent(item.event)}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
<AdvancedTable
|
||||
items={auditLogs}
|
||||
{requestOptions}
|
||||
onRefresh={async (options) =>
|
||||
id="audit-log-list-{isAdmin ? 'admin' : 'user'}"
|
||||
bind:this={tableRef}
|
||||
fetchCallback={async (options) =>
|
||||
isAdmin
|
||||
? (auditLogs = await auditLogService.listAllLogs(options))
|
||||
: (auditLogs = await auditLogService.list(options))}
|
||||
columns={[
|
||||
{ label: m.time(), sortColumn: 'createdAt' },
|
||||
...(isAdmin ? [{ label: 'Username' }] : []),
|
||||
{ label: m.event(), sortColumn: 'event' },
|
||||
{ label: m.approximate_location(), sortColumn: 'city' },
|
||||
{ label: m.ip_address(), sortColumn: 'ipAddress' },
|
||||
{ label: m.device(), sortColumn: 'device' },
|
||||
{ label: m.client() }
|
||||
]}
|
||||
? await auditLogService.listAllLogs({
|
||||
...options,
|
||||
filters: wrapFilters(filters)
|
||||
})
|
||||
: await auditLogService.list(options)}
|
||||
defaultSort={{ column: 'createdAt', direction: 'desc' }}
|
||||
withoutSearch
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{new Date(item.createdAt).toLocaleString()}</Table.Cell>
|
||||
{#if isAdmin}
|
||||
<Table.Cell>
|
||||
{#if item.username}
|
||||
{item.username}
|
||||
{:else}
|
||||
Unknown User
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<Table.Cell>
|
||||
<Badge class="rounded-full" variant="outline">{translateAuditLogEvent(item.event)}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if item.city && item.country}
|
||||
{item.city}, {item.country}
|
||||
{:else if item.country}
|
||||
{item.country}
|
||||
{:else}
|
||||
{m.unknown()}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{item.ipAddress}</Table.Cell>
|
||||
<Table.Cell>{item.device}</Table.Cell>
|
||||
<Table.Cell>{item.data.clientName}</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
{columns}
|
||||
/>
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
let {
|
||||
label,
|
||||
accept,
|
||||
onchange
|
||||
onchange,
|
||||
id = 'file-input'
|
||||
}: {
|
||||
label: string;
|
||||
accept?: string;
|
||||
onchange: (file: File | string | null) => void;
|
||||
id?: string;
|
||||
} = $props();
|
||||
|
||||
let url = $state('');
|
||||
@@ -47,7 +49,7 @@
|
||||
|
||||
<div class="flex">
|
||||
<FileInput
|
||||
id="logo"
|
||||
{id}
|
||||
variant="secondary"
|
||||
{accept}
|
||||
onchange={handleFileChange}
|
||||
@@ -64,9 +66,9 @@
|
||||
<LucideChevronDown class="size-4" /></Popover.Trigger
|
||||
>
|
||||
<Popover.Content class="w-80">
|
||||
<Label for="file-url" class="text-xs">URL</Label>
|
||||
<Label for="{id}-url" class="text-xs">URL</Label>
|
||||
<Input
|
||||
id="file-url"
|
||||
id="{id}-url"
|
||||
placeholder=""
|
||||
value={url}
|
||||
oninput={(e) => (url = e.currentTarget.value)}
|
||||
|
||||
@@ -12,14 +12,10 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class={'bg-muted flex items-center justify-center rounded-2xl p-3'}>
|
||||
<div class={cn('bg-muted flex items-center justify-center rounded-2xl p-3', props.class)}>
|
||||
{#if error}
|
||||
<LucideImageOff class={cn('text-muted-foreground p-5', props.class)} />
|
||||
<LucideImageOff class="text-muted-foreground p-5" />
|
||||
{:else}
|
||||
<img
|
||||
{...props}
|
||||
class={cn('object-contain aspect-square', props.class)}
|
||||
onerror={() => (error = true)}
|
||||
/>
|
||||
<img {...props} class="aspect-square object-contain" onerror={() => (error = true)} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
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 { type Snippet } from 'svelte';
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
import * as Card from './ui/card';
|
||||
|
||||
let {
|
||||
children,
|
||||
showAlternativeSignInMethodButton = false,
|
||||
animate = false
|
||||
showAlternativeSignInMethodButton = false
|
||||
}: {
|
||||
children: Snippet;
|
||||
showAlternativeSignInMethodButton?: boolean;
|
||||
animate?: boolean;
|
||||
} = $props();
|
||||
|
||||
let isInitialLoad = $state(false);
|
||||
let animate = $derived(isInitialLoad && !$appConfigStore.disableAnimations);
|
||||
|
||||
afterNavigate((e) => {
|
||||
isInitialLoad = !e?.from?.url;
|
||||
});
|
||||
|
||||
const isDesktop = new MediaQuery('min-width: 1024px');
|
||||
let alternativeSignInButton = $state({
|
||||
href: '/login/alternative',
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { Badge, type BadgeVariant } from '$lib/components/ui/badge';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { SignupTokenDto } from '$lib/types/signup-token.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { Copy, Ellipsis, Trash2 } from '@lucide/svelte';
|
||||
import { Copy, Trash2 } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
signupTokens = $bindable(),
|
||||
signupTokensRequestOptions,
|
||||
onTokenDeleted
|
||||
open = $bindable()
|
||||
}: {
|
||||
open: boolean;
|
||||
signupTokens: Paginated<SignupTokenDto>;
|
||||
signupTokensRequestOptions: SearchPaginationSortRequest;
|
||||
onTokenDeleted?: () => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
let tableRef: AdvancedTable<SignupTokenDto>;
|
||||
|
||||
function formatDate(dateStr: string | undefined) {
|
||||
if (!dateStr) return m.never();
|
||||
@@ -44,12 +40,8 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await userService.deleteSignupToken(token.id);
|
||||
await tableRef.refresh();
|
||||
toast.success(m.signup_token_deleted_successfully());
|
||||
|
||||
// Refresh the tokens
|
||||
if (onTokenDeleted) {
|
||||
await onTokenDeleted();
|
||||
}
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -98,8 +90,69 @@
|
||||
axiosErrorToast(err);
|
||||
});
|
||||
}
|
||||
|
||||
const columns: AdvancedTableColumn<SignupTokenDto>[] = [
|
||||
{ label: m.token(), column: 'token', cell: TokenCell },
|
||||
{ label: m.status(), key: 'status', cell: StatusCell },
|
||||
{
|
||||
label: m.usage(),
|
||||
column: 'usageCount',
|
||||
sortable: true,
|
||||
cell: UsageCell
|
||||
},
|
||||
{
|
||||
label: m.expires(),
|
||||
column: 'expiresAt',
|
||||
sortable: true,
|
||||
value: (item) => formatDate(item.expiresAt)
|
||||
},
|
||||
{ label: 'Usage Limit', column: 'usageLimit' },
|
||||
{
|
||||
label: m.created(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
value: (item) => formatDate(item.createdAt)
|
||||
}
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<SignupTokenDto> = (_) => [
|
||||
{
|
||||
label: m.copy(),
|
||||
icon: Copy,
|
||||
onClick: (token) => copySignupLink(token)
|
||||
},
|
||||
{
|
||||
label: m.delete(),
|
||||
icon: Trash2,
|
||||
variant: 'danger',
|
||||
onClick: (token) => deleteToken(token)
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
{#snippet TokenCell({ item }: { item: SignupTokenDto })}
|
||||
<span class="font-mono text-xs">
|
||||
{item.token.substring(0, 3)}...{item.token.substring(Math.max(item.token.length - 4, 0))}
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet StatusCell({ item }: { item: SignupTokenDto })}
|
||||
{@const status = getTokenStatus(item)}
|
||||
{@const statusBadge = getStatusBadge(status)}
|
||||
<Badge class="rounded-full" variant={statusBadge.variant}>
|
||||
{statusBadge.text}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
{#snippet UsageCell({ item }: { item: SignupTokenDto })}
|
||||
<div class="flex items-center gap-1">
|
||||
{item.usageCount}
|
||||
{m.of()}
|
||||
{item.usageLimit}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Dialog.Root {open} {onOpenChange}>
|
||||
<Dialog.Content class="sm-min-w[500px] max-h-[90vh] min-w-[90vw] overflow-auto lg:min-w-[1000px]">
|
||||
<Dialog.Header>
|
||||
@@ -111,70 +164,13 @@
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<AdvancedTable
|
||||
items={signupTokens}
|
||||
requestOptions={signupTokensRequestOptions}
|
||||
id="signup-token-list"
|
||||
withoutSearch={true}
|
||||
onRefresh={async (options) => {
|
||||
const result = await userService.listSignupTokens(options);
|
||||
signupTokens = result;
|
||||
return result;
|
||||
}}
|
||||
columns={[
|
||||
{ label: m.token() },
|
||||
{ label: m.status() },
|
||||
{ label: m.usage(), sortColumn: 'usageCount' },
|
||||
{ label: m.expires(), sortColumn: 'expiresAt' },
|
||||
{ label: m.created(), sortColumn: 'createdAt' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell class="font-mono text-xs">
|
||||
{item.token.substring(0, 2)}...{item.token.substring(item.token.length - 4)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{@const status = getTokenStatus(item)}
|
||||
{@const statusBadge = getStatusBadge(status)}
|
||||
<Badge class="rounded-full" variant={statusBadge.variant}>
|
||||
{statusBadge.text}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-1">
|
||||
{`${item.usageCount} ${m.of()} ${item.usageLimit}`}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
{formatDate(item.expiresAt)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-sm">
|
||||
{formatDate(item.createdAt)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
||||
<Ellipsis class="size-4" />
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => copySignupLink(item)}>
|
||||
<Copy class="mr-2 size-4" />
|
||||
{m.copy()}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
onclick={() => deleteToken(item)}
|
||||
>
|
||||
<Trash2 class="mr-2 size-4" />
|
||||
{m.delete()}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
fetchCallback={userService.listSignupTokens}
|
||||
bind:this={tableRef}
|
||||
{columns}
|
||||
{actions}
|
||||
/>
|
||||
</div>
|
||||
<Dialog.Footer class="mt-3">
|
||||
<Button onclick={() => (open = false)}>
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
import { mode } from 'mode-watcher';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onTokenCreated
|
||||
open = $bindable()
|
||||
}: {
|
||||
open: boolean;
|
||||
onTokenCreated?: () => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
@@ -37,12 +35,11 @@
|
||||
|
||||
async function createSignupToken() {
|
||||
try {
|
||||
signupToken = await userService.createSignupToken(availableExpirations[selectedExpiration], usageLimit);
|
||||
signupToken = await userService.createSignupToken(
|
||||
availableExpirations[selectedExpiration],
|
||||
usageLimit
|
||||
);
|
||||
signupLink = `${page.url.origin}/st/${signupToken}`;
|
||||
|
||||
if (onTokenCreated) {
|
||||
await onTokenCreated();
|
||||
}
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" generics="TData extends Record<string, any>">
|
||||
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
||||
|
||||
let {
|
||||
columns,
|
||||
selectedColumns = $bindable([])
|
||||
}: { columns: AdvancedTableColumn<TData>[]; selectedColumns: string[] } = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class={buttonVariants({
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
class: 'ml-auto h-8'
|
||||
})}
|
||||
>
|
||||
<Settings2Icon />
|
||||
<span class="hidden md:flex">{m.view()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>{m.toggle_columns()}</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
{#each columns as column (column)}
|
||||
<DropdownMenu.CheckboxItem
|
||||
closeOnSelect={false}
|
||||
checked={selectedColumns.includes(column.column ?? column.key!)}
|
||||
onCheckedChange={(v) => {
|
||||
const key = column.column ?? column.key!;
|
||||
if (v) {
|
||||
selectedColumns = [...selectedColumns, key];
|
||||
} else {
|
||||
selectedColumns = selectedColumns.filter((c) => c !== key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
128
frontend/src/lib/components/table/advanced-table-filter.svelte
Normal file
128
frontend/src/lib/components/table/advanced-table-filter.svelte
Normal file
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Command from '$lib/components/ui/command/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import ListFilterIcon from '@lucide/svelte/icons/list-filter';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
let {
|
||||
title,
|
||||
options,
|
||||
selectedValues = new Set<string | boolean>(),
|
||||
showCheckboxes = true,
|
||||
onChanged = (selected: Set<string | boolean>) => {}
|
||||
}: {
|
||||
title: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
icon?: Component;
|
||||
}[];
|
||||
selectedValues?: Set<string | boolean>;
|
||||
showCheckboxes?: boolean;
|
||||
onChanged?: (selected: Set<string | boolean>) => void;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 border-dashed"
|
||||
data-testid={`facet-${title.toLowerCase()}-trigger`}
|
||||
>
|
||||
<ListFilterIcon />
|
||||
{title}
|
||||
{#if selectedValues.size > 0}
|
||||
<Separator orientation="vertical" class="mx-2 h-4" />
|
||||
<Badge variant="secondary" class="rounded-sm px-1 font-normal lg:hidden">
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
<div class="hidden space-x-1 lg:flex">
|
||||
{#if selectedValues.size > 2}
|
||||
<Badge variant="secondary" class="rounded-sm px-1 font-normal">
|
||||
Count: {selectedValues.size}
|
||||
</Badge>
|
||||
{:else}
|
||||
{#each options.filter((opt) => selectedValues.has(opt.value)) as option (option)}
|
||||
<Badge variant="secondary" class="rounded-sm px-1 font-normal">
|
||||
{option.label}
|
||||
</Badge>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
class="w-[200px] p-0"
|
||||
align="start"
|
||||
data-testid={`facet-${title.toLowerCase()}-content`}
|
||||
>
|
||||
<Command.Root>
|
||||
<Command.List>
|
||||
<Command.Empty>{m.no_items_found()}</Command.Empty>
|
||||
<Command.Group>
|
||||
{#each options as option (option)}
|
||||
{@const isSelected = selectedValues.has(option.value)}
|
||||
<Command.Item
|
||||
data-testid={`facet-${title.toLowerCase()}-option-${String(option.value)}`}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues = new Set([...selectedValues].filter((v) => v !== option.value));
|
||||
} else {
|
||||
selectedValues = new Set([...selectedValues, option.value]);
|
||||
}
|
||||
onChanged(selectedValues);
|
||||
}}
|
||||
>
|
||||
{#if showCheckboxes}
|
||||
<div
|
||||
class={cn(
|
||||
'border-primary mr-2 flex size-4 items-center justify-center rounded-sm border',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon class="size-4" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if option.icon}
|
||||
{@const Icon = option.icon}
|
||||
<Icon class="text-muted-foreground" />
|
||||
{/if}
|
||||
|
||||
<span>{option.label}</span>
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
{#if selectedValues.size > 0}
|
||||
<Command.Separator />
|
||||
<Command.Group>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
selectedValues = new Set();
|
||||
onChanged(selectedValues);
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
{m.clear_filters()}
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
{/if}
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" generics="TData extends Record<string, any>">
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||
import type { ListRequestOptions } from '$lib/types/list-request.type';
|
||||
import { debounced } from '$lib/utils/debounce-util';
|
||||
import AdvancedTableColumnSelection from './advanced-table-column-selection.svelte';
|
||||
import AdvancedTableFilter from './advanced-table-filter.svelte';
|
||||
|
||||
let {
|
||||
columns,
|
||||
visibleColumns = $bindable(),
|
||||
requestOptions,
|
||||
searchValue = $bindable(),
|
||||
withoutSearch = false,
|
||||
onFilterChange,
|
||||
refresh
|
||||
}: {
|
||||
columns: AdvancedTableColumn<TData>[];
|
||||
visibleColumns: string[];
|
||||
requestOptions: ListRequestOptions;
|
||||
searchValue?: string;
|
||||
withoutSearch?: boolean;
|
||||
onFilterChange?: (selected: Set<string | boolean>, column: string) => void;
|
||||
refresh: () => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
let filterableColumns = $derived(
|
||||
columns
|
||||
.filter((c) => c.filterableValues)
|
||||
.map((c) => ({
|
||||
name: c.label!,
|
||||
column: c.column!,
|
||||
options: c.filterableValues!
|
||||
}))
|
||||
);
|
||||
|
||||
const onSearch = debounced(async (search: string) => {
|
||||
requestOptions.search = search;
|
||||
await refresh();
|
||||
searchValue = search;
|
||||
}, 300);
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end justify-between gap-2">
|
||||
<div class="flex flex-1 items-center gap-2 has-[>:nth-child(3)]:flex-wrap">
|
||||
{#if !withoutSearch}
|
||||
<Input
|
||||
value={searchValue}
|
||||
class="relative z-50 w-full sm:max-w-xs"
|
||||
placeholder={m.search()}
|
||||
type="text"
|
||||
oninput={(e: Event) => onSearch((e.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each filterableColumns as col}
|
||||
<AdvancedTableFilter
|
||||
title={col.name}
|
||||
options={col.options}
|
||||
selectedValues={new Set(requestOptions.filters?.[col.column] || [])}
|
||||
onChanged={(selected) => onFilterChange?.(selected, col.column)}
|
||||
/>
|
||||
{/each}
|
||||
<AdvancedTableColumnSelection {columns} bind:selectedColumns={visibleColumns} />
|
||||
</div>
|
||||
</div>
|
||||
356
frontend/src/lib/components/table/advanced-table.svelte
Normal file
356
frontend/src/lib/components/table/advanced-table.svelte
Normal file
@@ -0,0 +1,356 @@
|
||||
<script lang="ts" generics="T extends {id:string}">
|
||||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||
import * as Pagination from '$lib/components/ui/pagination';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import Empty from '$lib/icons/empty.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { ListRequestOptions, Paginated, SortRequest } from '$lib/types/list-request.type';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { ChevronDown, LucideEllipsis } from '@lucide/svelte';
|
||||
import { PersistedState } from 'runed';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Button, { buttonVariants } from '../ui/button/button.svelte';
|
||||
import * as DropdownMenu from '../ui/dropdown-menu/index.js';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import AdvancedTableToolbar from './advanced-table-toolbar.svelte';
|
||||
|
||||
let {
|
||||
id,
|
||||
selectedIds = $bindable(),
|
||||
withoutSearch = false,
|
||||
selectionDisabled = false,
|
||||
fetchCallback,
|
||||
defaultSort,
|
||||
columns,
|
||||
actions
|
||||
}: {
|
||||
id: string;
|
||||
selectedIds?: string[];
|
||||
withoutSearch?: boolean;
|
||||
selectionDisabled?: boolean;
|
||||
fetchCallback: (requestOptions: ListRequestOptions) => Promise<Paginated<T>>;
|
||||
defaultSort?: SortRequest;
|
||||
columns: AdvancedTableColumn<T>[];
|
||||
actions?: CreateAdvancedTableActions<T>;
|
||||
} = $props();
|
||||
|
||||
let items: Paginated<T> | undefined = $state();
|
||||
let searchValue = $state('');
|
||||
|
||||
const availablePageSizes: number[] = [20, 50, 100];
|
||||
|
||||
type TablePreferences = {
|
||||
visibleColumns: string[];
|
||||
paginationLimit: number;
|
||||
sort?: SortRequest;
|
||||
filters?: Record<string, (string | boolean)[]>;
|
||||
length?: number;
|
||||
};
|
||||
|
||||
const tablePreferences = new PersistedState<TablePreferences>(`table-${id}-preferences`, {
|
||||
visibleColumns: columns.filter((c) => !c.hidden).map((c) => c.column ?? c.key!),
|
||||
paginationLimit: 20,
|
||||
filters: initializeFilters()
|
||||
});
|
||||
|
||||
const requestOptions = $state<ListRequestOptions>({
|
||||
sort: tablePreferences.current.sort ?? defaultSort,
|
||||
pagination: { limit: tablePreferences.current.paginationLimit, page: 1 },
|
||||
filters: tablePreferences.current.filters
|
||||
});
|
||||
|
||||
let visibleColumns = $derived(
|
||||
columns.filter(
|
||||
(c) => tablePreferences.current.visibleColumns?.includes(c.column ?? c.key!) ?? []
|
||||
)
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const page = parseInt(urlParams.get(`${id}-page`) ?? '') || undefined;
|
||||
if (page) {
|
||||
requestOptions.pagination!.page = page;
|
||||
}
|
||||
await refresh();
|
||||
});
|
||||
|
||||
let allChecked = $derived.by(() => {
|
||||
if (!selectedIds || !items || items.data.length === 0) return false;
|
||||
for (const item of items!.data) {
|
||||
if (!selectedIds.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
async function onAllCheck(checked: boolean) {
|
||||
const pageIds = items!.data.map((item) => item.id);
|
||||
const current = selectedIds ?? [];
|
||||
|
||||
if (checked) {
|
||||
selectedIds = Array.from(new Set([...current, ...pageIds]));
|
||||
} else {
|
||||
selectedIds = current.filter((id) => !pageIds.includes(id));
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheck(checked: boolean, id: string) {
|
||||
const current = selectedIds ?? [];
|
||||
if (checked) {
|
||||
selectedIds = Array.from(new Set([...current, id]));
|
||||
} else {
|
||||
selectedIds = current.filter((selectedId) => selectedId !== id);
|
||||
}
|
||||
}
|
||||
|
||||
async function onPageChange(page: number) {
|
||||
changePageState(page);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function onPageSizeChange(size: number) {
|
||||
requestOptions.pagination = { limit: size, page: 1 };
|
||||
tablePreferences.current.paginationLimit = size;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function onFilterChange(selected: Set<string | boolean>, column: string) {
|
||||
requestOptions.filters = {
|
||||
...requestOptions.filters,
|
||||
[column]: Array.from(selected)
|
||||
};
|
||||
tablePreferences.current.filters = requestOptions.filters;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function onSort(column?: string) {
|
||||
if (!column) return;
|
||||
|
||||
const isSameColumn = requestOptions.sort?.column === column;
|
||||
const nextDirection: 'asc' | 'desc' =
|
||||
isSameColumn && requestOptions.sort?.direction === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
requestOptions.sort = { column, direction: nextDirection };
|
||||
tablePreferences.current.sort = requestOptions.sort;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
function changePageState(page: number) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set(`${id}-page`, page.toString());
|
||||
history.replaceState(history.state, '', url.toString());
|
||||
requestOptions.pagination!.page = page;
|
||||
}
|
||||
|
||||
function updateListLength(totalItems: number) {
|
||||
tablePreferences.current.length =
|
||||
totalItems > tablePreferences.current.paginationLimit
|
||||
? tablePreferences.current.paginationLimit
|
||||
: totalItems;
|
||||
}
|
||||
|
||||
function initializeFilters() {
|
||||
const filters: Record<string, (string | boolean)[]> = {};
|
||||
columns.forEach((c) => {
|
||||
if (c.filterableValues) {
|
||||
filters[c.column!] = [];
|
||||
}
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
export async function refresh() {
|
||||
items = await fetchCallback(requestOptions);
|
||||
changePageState(items.pagination.currentPage);
|
||||
updateListLength(items.pagination.totalItems);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AdvancedTableToolbar
|
||||
{columns}
|
||||
bind:visibleColumns={tablePreferences.current.visibleColumns}
|
||||
{requestOptions}
|
||||
{searchValue}
|
||||
{withoutSearch}
|
||||
{refresh}
|
||||
{onFilterChange}
|
||||
/>
|
||||
|
||||
{#if (items?.pagination.totalItems === 0 && searchValue === '') || tablePreferences.current.length === 0}
|
||||
<div class="my-5 flex flex-col items-center">
|
||||
<Empty class="text-muted-foreground h-20" />
|
||||
<p class="text-muted-foreground mt-3 text-sm">{m.no_items_found()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#if !items}
|
||||
<div>
|
||||
{#each Array((tablePreferences.current.length || 10) + 1) as _}
|
||||
<div>
|
||||
<Skeleton class="mt-3 h-[45px] w-full rounded-lg" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fade>
|
||||
<Table.Root class="min-w-full table-auto overflow-x-auto">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{#if selectedIds}
|
||||
<Table.Head class="w-12">
|
||||
<Checkbox
|
||||
disabled={selectionDisabled}
|
||||
checked={allChecked}
|
||||
onCheckedChange={(c: boolean) => onAllCheck(c as boolean)}
|
||||
/>
|
||||
</Table.Head>
|
||||
{/if}
|
||||
|
||||
{#each visibleColumns as column}
|
||||
<Table.Head class={cn(column.sortable && 'p-0')}>
|
||||
{#if column.sortable}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent"
|
||||
onclick={() => onSort(column.column)}
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{column.label}
|
||||
<ChevronDown
|
||||
class={cn(
|
||||
'ml-2 size-4 transition-all',
|
||||
requestOptions.sort?.column === column.column
|
||||
? requestOptions.sort?.direction === 'asc'
|
||||
? 'rotate-180 opacity-100'
|
||||
: 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
{:else}
|
||||
{column.label}
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
{#if actions}
|
||||
<Table.Head align="right" class="w-12">
|
||||
<span class="sr-only">{m.actions()}</span>
|
||||
</Table.Head>
|
||||
{/if}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each items.data as item}
|
||||
<Table.Row class={selectedIds?.includes(item.id) ? 'bg-muted/20' : ''}>
|
||||
{#if selectedIds}
|
||||
<Table.Cell class="w-12">
|
||||
<Checkbox
|
||||
disabled={selectionDisabled}
|
||||
checked={selectedIds.includes(item.id)}
|
||||
onCheckedChange={(c: boolean) => onCheck(c, item.id)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
{#each visibleColumns as column}
|
||||
<Table.Cell>
|
||||
{#if column.value}
|
||||
{column.value(item)}
|
||||
{:else if column.cell}
|
||||
{@render column.cell({ item })}
|
||||
{:else if column.column && typeof item[column.column] === 'boolean'}
|
||||
{item[column.column] ? m.enabled() : m.disabled()}
|
||||
{:else if column.column}
|
||||
{item[column.column]}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
{#if actions}
|
||||
<Table.Cell align="right" class="w-12 py-0">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class={buttonVariants({ variant: 'ghost', size: 'icon' })}
|
||||
>
|
||||
<LucideEllipsis class="size-4" />
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
{#each actions(item).filter((a) => !a.hidden) as action}
|
||||
<DropdownMenu.Item
|
||||
onclick={() => action.onClick(item)}
|
||||
disabled={action.disabled}
|
||||
class={action.variant === 'danger'
|
||||
? 'text-red-500 focus:!text-red-700'
|
||||
: ''}
|
||||
>
|
||||
{#if action.icon}
|
||||
{@const Icon = action.icon}
|
||||
<Icon class="mr-2 size-4" />
|
||||
{/if}
|
||||
{action.label}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-5 flex flex-col-reverse items-center justify-between gap-3 sm:flex-row">
|
||||
<div class="flex items-center space-x-2">
|
||||
<p class="text-sm font-medium">{m.items_per_page()}</p>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={items?.pagination.itemsPerPage.toString()}
|
||||
onValueChange={(v) => onPageSizeChange(Number(v))}
|
||||
>
|
||||
<Select.Trigger class="h-9 w-[80px]">
|
||||
{items?.pagination.itemsPerPage}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each availablePageSizes as size}
|
||||
<Select.Item value={size.toString()}>{size}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Pagination.Root
|
||||
class="mx-0 w-auto"
|
||||
count={items?.pagination.totalItems || 0}
|
||||
perPage={items?.pagination.itemsPerPage}
|
||||
{onPageChange}
|
||||
page={items?.pagination.currentPage}
|
||||
>
|
||||
{#snippet children({ pages })}
|
||||
<Pagination.Content class="flex justify-end">
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type !== 'ellipsis' && page.value != 0}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={items?.pagination.currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
</div>
|
||||
{/if}
|
||||
7
frontend/src/lib/components/ui/skeleton/index.ts
Normal file
7
frontend/src/lib/components/ui/skeleton/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./skeleton.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Skeleton,
|
||||
};
|
||||
17
frontend/src/lib/components/ui/skeleton/skeleton.svelte
Normal file
17
frontend/src/lib/components/ui/skeleton/skeleton.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils/style.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="skeleton"
|
||||
class={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...restProps}
|
||||
></div>
|
||||
@@ -14,7 +14,7 @@
|
||||
bind:this={ref}
|
||||
data-slot="table-cell"
|
||||
class={cn(
|
||||
'p-4 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
'py-3 px-4 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { UserGroup } from '$lib/types/user-group.type';
|
||||
import { onMount } from 'svelte';
|
||||
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||
import type { UserGroupWithUserCount } from '$lib/types/user-group.type';
|
||||
|
||||
let {
|
||||
selectionDisabled = false,
|
||||
@@ -17,30 +15,27 @@
|
||||
|
||||
const userGroupService = new UserGroupService();
|
||||
|
||||
let groups: Paginated<UserGroup> | undefined = $state();
|
||||
let requestOptions: SearchPaginationSortRequest = $state({
|
||||
sort: {
|
||||
column: 'friendlyName',
|
||||
direction: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
groups = await userGroupService.list(requestOptions);
|
||||
});
|
||||
const columns: AdvancedTableColumn<UserGroupWithUserCount>[] = [
|
||||
{ label: 'ID', column: 'id', hidden: true },
|
||||
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
|
||||
{ label: m.name(), column: 'name', sortable: true },
|
||||
{ label: m.user_count(), column: 'userCount', sortable: true },
|
||||
{
|
||||
label: m.created(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
value: (item) => new Date(item.createdAt).toLocaleString()
|
||||
},
|
||||
{ label: m.ldap_id(), column: 'ldapId', hidden: true }
|
||||
];
|
||||
</script>
|
||||
|
||||
{#if groups}
|
||||
<AdvancedTable
|
||||
items={groups}
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (groups = await userGroupService.list(o))}
|
||||
columns={[{ label: m.name(), sortColumn: 'friendlyName' }]}
|
||||
bind:selectedIds={selectedGroupIds}
|
||||
{selectionDisabled}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.friendlyName}</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
{/if}
|
||||
<AdvancedTable
|
||||
id="user-group-selection"
|
||||
fetchCallback={userGroupService.list}
|
||||
defaultSort={{ column: 'friendlyName', direction: 'asc' }}
|
||||
bind:selectedIds={selectedGroupIds}
|
||||
{selectionDisabled}
|
||||
{columns}
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { ApiKey, ApiKeyCreate, ApiKeyResponse } from '$lib/types/api-key.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class ApiKeyService extends APIService {
|
||||
async list(options?: SearchPaginationSortRequest) {
|
||||
const res = await this.api.get('/api-keys', {
|
||||
params: options
|
||||
});
|
||||
list = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/api-keys', { params: options });
|
||||
return res.data as Paginated<ApiKey>;
|
||||
}
|
||||
};
|
||||
|
||||
async create(data: ApiKeyCreate): Promise<ApiKeyResponse> {
|
||||
create = async (data: ApiKeyCreate): Promise<ApiKeyResponse> => {
|
||||
const res = await this.api.post('/api-keys', data);
|
||||
return res.data as ApiKeyResponse;
|
||||
}
|
||||
};
|
||||
|
||||
async revoke(id: string): Promise<void> {
|
||||
revoke = async (id: string): Promise<void> => {
|
||||
await this.api.delete(`/api-keys/${id}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import axios from 'axios';
|
||||
|
||||
abstract class APIService {
|
||||
api = axios.create({
|
||||
baseURL: '/api'
|
||||
});
|
||||
protected api = axios.create({ baseURL: '/api' });
|
||||
|
||||
constructor() {
|
||||
if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) {
|
||||
this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL;
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) {
|
||||
this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default APIService;
|
||||
|
||||
@@ -3,39 +3,33 @@ import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class AppConfigService extends APIService {
|
||||
async list(showAll = false) {
|
||||
list = async (showAll = false) => {
|
||||
let url = '/application-configuration';
|
||||
if (showAll) {
|
||||
url += '/all';
|
||||
}
|
||||
|
||||
if (showAll) url += '/all';
|
||||
const { data } = await this.api.get<AppConfigRawResponse>(url);
|
||||
return this.parseConfigList(data);
|
||||
}
|
||||
return parseConfigList(data);
|
||||
};
|
||||
|
||||
async update(appConfig: AllAppConfig) {
|
||||
update = async (appConfig: AllAppConfig) => {
|
||||
// Convert all values to string, stringifying JSON where needed
|
||||
const appConfigConvertedToString: Record<string, string> = {};
|
||||
for (const key in appConfig) {
|
||||
const value = (appConfig as any)[key];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
appConfigConvertedToString[key] = JSON.stringify(value);
|
||||
} else {
|
||||
appConfigConvertedToString[key] = String(value);
|
||||
}
|
||||
appConfigConvertedToString[key] =
|
||||
typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
|
||||
return this.parseConfigList(res.data);
|
||||
}
|
||||
return parseConfigList(res.data);
|
||||
};
|
||||
|
||||
async updateFavicon(favicon: File) {
|
||||
updateFavicon = async (favicon: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', favicon!);
|
||||
|
||||
await this.api.put(`/application-images/favicon`, formData);
|
||||
}
|
||||
|
||||
async updateLogo(logo: File, light = true) {
|
||||
updateLogo = async (logo: File, light = true) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', logo!);
|
||||
|
||||
@@ -43,52 +37,52 @@ export default class AppConfigService extends APIService {
|
||||
params: { light }
|
||||
});
|
||||
cachedApplicationLogo.bustCache(light);
|
||||
}
|
||||
};
|
||||
|
||||
async updateBackgroundImage(backgroundImage: File) {
|
||||
updateBackgroundImage = async (backgroundImage: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', backgroundImage!);
|
||||
|
||||
await this.api.put(`/application-images/background`, formData);
|
||||
cachedBackgroundImage.bustCache();
|
||||
}
|
||||
};
|
||||
|
||||
async sendTestEmail() {
|
||||
sendTestEmail = async () => {
|
||||
await this.api.post('/application-configuration/test-email');
|
||||
}
|
||||
};
|
||||
|
||||
async syncLdap() {
|
||||
syncLdap = async () => {
|
||||
await this.api.post('/application-configuration/sync-ldap');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private parseConfigList(data: AppConfigRawResponse) {
|
||||
const appConfig: Partial<AllAppConfig> = {};
|
||||
data.forEach(({ key, value }) => {
|
||||
(appConfig as any)[key] = this.parseValue(value);
|
||||
});
|
||||
function parseConfigList(data: AppConfigRawResponse) {
|
||||
const appConfig: Partial<AllAppConfig> = {};
|
||||
data.forEach(({ key, value }) => {
|
||||
(appConfig as any)[key] = parseValue(value);
|
||||
});
|
||||
|
||||
return appConfig as AllAppConfig;
|
||||
}
|
||||
return appConfig as AllAppConfig;
|
||||
}
|
||||
|
||||
private parseValue(value: string) {
|
||||
// Try to parse JSON first
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
value = String(parsed);
|
||||
} catch {}
|
||||
|
||||
// Handle rest of the types
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
} else if (value === 'false') {
|
||||
return false;
|
||||
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
||||
return parseFloat(value);
|
||||
} else {
|
||||
return value;
|
||||
function parseValue(value: string) {
|
||||
// Try to parse JSON first
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
value = String(parsed);
|
||||
} catch {}
|
||||
|
||||
// Handle rest of the types
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
} else if (value === 'false') {
|
||||
return false;
|
||||
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
||||
return parseFloat(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import type { AuditLog, AuditLogFilter } from '$lib/types/audit-log.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { AuditLog } from '$lib/types/audit-log.type';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import APIService from './api-service';
|
||||
|
||||
class AuditLogService extends APIService {
|
||||
async list(options?: SearchPaginationSortRequest) {
|
||||
const res = await this.api.get('/audit-logs', {
|
||||
params: options
|
||||
});
|
||||
export default class AuditLogService extends APIService {
|
||||
list = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/audit-logs', { params: options });
|
||||
return res.data as Paginated<AuditLog>;
|
||||
}
|
||||
};
|
||||
|
||||
async listAllLogs(options?: SearchPaginationSortRequest, filters?: AuditLogFilter) {
|
||||
const res = await this.api.get('/audit-logs/all', {
|
||||
params: {
|
||||
...options,
|
||||
filters
|
||||
}
|
||||
});
|
||||
listAllLogs = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/audit-logs/all', { params: options });
|
||||
return res.data as Paginated<AuditLog>;
|
||||
}
|
||||
};
|
||||
|
||||
async listClientNames() {
|
||||
listClientNames = async () => {
|
||||
const res = await this.api.get<string[]>('/audit-logs/filters/client-names');
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
|
||||
async listUsers() {
|
||||
listUsers = async () => {
|
||||
const res = await this.api.get<Record<string, string>>('/audit-logs/filters/users');
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default AuditLogService;
|
||||
|
||||
@@ -2,18 +2,18 @@ import type { CustomClaim } from '$lib/types/custom-claim.type';
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class CustomClaimService extends APIService {
|
||||
async getSuggestions() {
|
||||
getSuggestions = async () => {
|
||||
const res = await this.api.get('/custom-claims/suggestions');
|
||||
return res.data as string[];
|
||||
}
|
||||
};
|
||||
|
||||
async updateUserCustomClaims(userId: string, claims: CustomClaim[]) {
|
||||
updateUserCustomClaims = async (userId: string, claims: CustomClaim[]) => {
|
||||
const res = await this.api.put(`/custom-claims/user/${userId}`, claims);
|
||||
return res.data as CustomClaim[];
|
||||
}
|
||||
};
|
||||
|
||||
async updateUserGroupCustomClaims(userGroupId: string, claims: CustomClaim[]) {
|
||||
updateUserGroupCustomClaims = async (userGroupId: string, claims: CustomClaim[]) => {
|
||||
const res = await this.api.put(`/custom-claims/user-group/${userGroupId}`, claims);
|
||||
return res.data as CustomClaim[];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import type {
|
||||
AccessibleOidcClient,
|
||||
AuthorizeResponse,
|
||||
@@ -9,12 +10,11 @@ import type {
|
||||
OidcClientWithAllowedUserGroupsCount,
|
||||
OidcDeviceCodeInfo
|
||||
} from '$lib/types/oidc.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import APIService from './api-service';
|
||||
|
||||
class OidcService extends APIService {
|
||||
async authorize(
|
||||
authorize = async (
|
||||
clientId: string,
|
||||
scope: string,
|
||||
callbackURL: string,
|
||||
@@ -22,7 +22,7 @@ class OidcService extends APIService {
|
||||
codeChallenge?: string,
|
||||
codeChallengeMethod?: string,
|
||||
reauthenticationToken?: string
|
||||
) {
|
||||
) => {
|
||||
const res = await this.api.post('/oidc/authorize', {
|
||||
scope,
|
||||
nonce,
|
||||
@@ -34,101 +34,99 @@ class OidcService extends APIService {
|
||||
});
|
||||
|
||||
return res.data as AuthorizeResponse;
|
||||
}
|
||||
};
|
||||
|
||||
async isAuthorizationRequired(clientId: string, scope: string) {
|
||||
isAuthorizationRequired = async (clientId: string, scope: string) => {
|
||||
const res = await this.api.post('/oidc/authorization-required', {
|
||||
scope,
|
||||
clientId
|
||||
});
|
||||
|
||||
return res.data.authorizationRequired as boolean;
|
||||
}
|
||||
};
|
||||
|
||||
async listClients(options?: SearchPaginationSortRequest) {
|
||||
listClients = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/oidc/clients', {
|
||||
params: options
|
||||
});
|
||||
return res.data as Paginated<OidcClientWithAllowedUserGroupsCount>;
|
||||
}
|
||||
};
|
||||
|
||||
async createClient(client: OidcClientCreate) {
|
||||
return (await this.api.post('/oidc/clients', client)).data as OidcClient;
|
||||
}
|
||||
createClient = async (client: OidcClientCreate) =>
|
||||
(await this.api.post('/oidc/clients', client)).data as OidcClient;
|
||||
|
||||
async removeClient(id: string) {
|
||||
removeClient = async (id: string) => {
|
||||
await this.api.delete(`/oidc/clients/${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
async getClient(id: string) {
|
||||
return (await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups;
|
||||
}
|
||||
getClient = async (id: string) =>
|
||||
(await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups;
|
||||
|
||||
async getClientMetaData(id: string) {
|
||||
return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
|
||||
}
|
||||
getClientMetaData = async (id: string) =>
|
||||
(await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
|
||||
|
||||
async updateClient(id: string, client: OidcClientUpdate) {
|
||||
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
|
||||
}
|
||||
updateClient = async (id: string, client: OidcClientUpdate) =>
|
||||
(await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
|
||||
|
||||
async updateClientLogo(client: OidcClient, image: File | null) {
|
||||
if (client.hasLogo && !image) {
|
||||
await this.removeClientLogo(client.id);
|
||||
updateClientLogo = async (client: OidcClient, image: File | null, light: boolean = true) => {
|
||||
const hasLogo = light ? client.hasLogo : client.hasDarkLogo;
|
||||
|
||||
if (hasLogo && !image) {
|
||||
await this.removeClientLogo(client.id, light);
|
||||
return;
|
||||
}
|
||||
if (!client.hasLogo && !image) {
|
||||
if (!hasLogo && !image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', image!);
|
||||
|
||||
await this.api.post(`/oidc/clients/${client.id}/logo`, formData);
|
||||
cachedOidcClientLogo.bustCache(client.id);
|
||||
}
|
||||
await this.api.post(`/oidc/clients/${client.id}/logo`, formData, {
|
||||
params: { light }
|
||||
});
|
||||
cachedOidcClientLogo.bustCache(client.id, light);
|
||||
};
|
||||
|
||||
async removeClientLogo(id: string) {
|
||||
await this.api.delete(`/oidc/clients/${id}/logo`);
|
||||
cachedOidcClientLogo.bustCache(id);
|
||||
}
|
||||
removeClientLogo = async (id: string, light: boolean = true) => {
|
||||
await this.api.delete(`/oidc/clients/${id}/logo`, {
|
||||
params: { light }
|
||||
});
|
||||
cachedOidcClientLogo.bustCache(id, light);
|
||||
};
|
||||
|
||||
async createClientSecret(id: string) {
|
||||
return (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
|
||||
}
|
||||
createClientSecret = async (id: string) =>
|
||||
(await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
|
||||
|
||||
async updateAllowedUserGroups(id: string, userGroupIds: string[]) {
|
||||
updateAllowedUserGroups = async (id: string, userGroupIds: string[]) => {
|
||||
const res = await this.api.put(`/oidc/clients/${id}/allowed-user-groups`, { userGroupIds });
|
||||
return res.data as OidcClientWithAllowedUserGroups;
|
||||
}
|
||||
};
|
||||
|
||||
async verifyDeviceCode(userCode: string) {
|
||||
verifyDeviceCode = async (userCode: string) => {
|
||||
return await this.api.post(`/oidc/device/verify?code=${userCode}`);
|
||||
}
|
||||
};
|
||||
|
||||
async getDeviceCodeInfo(userCode: string): Promise<OidcDeviceCodeInfo> {
|
||||
getDeviceCodeInfo = async (userCode: string): Promise<OidcDeviceCodeInfo> => {
|
||||
const response = await this.api.get(`/oidc/device/info?code=${userCode}`);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
async getClientPreview(id: string, userId: string, scopes: string) {
|
||||
getClientPreview = async (id: string, userId: string, scopes: string) => {
|
||||
const response = await this.api.get(`/oidc/clients/${id}/preview/${userId}`, {
|
||||
params: { scopes }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async listOwnAccessibleClients(options?: SearchPaginationSortRequest) {
|
||||
const res = await this.api.get('/oidc/users/me/clients', {
|
||||
params: options
|
||||
});
|
||||
};
|
||||
|
||||
listOwnAccessibleClients = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/oidc/users/me/clients', { params: options });
|
||||
return res.data as Paginated<AccessibleOidcClient>;
|
||||
}
|
||||
};
|
||||
|
||||
async revokeOwnAuthorizedClient(clientId: string) {
|
||||
revokeOwnAuthorizedClient = async (clientId: string) => {
|
||||
await this.api.delete(`/oidc/users/me/authorized-clients/${clientId}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default OidcService;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import type {
|
||||
UserGroupCreate,
|
||||
UserGroupWithUserCount,
|
||||
@@ -7,34 +7,32 @@ import type {
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class UserGroupService extends APIService {
|
||||
async list(options?: SearchPaginationSortRequest) {
|
||||
const res = await this.api.get('/user-groups', {
|
||||
params: options
|
||||
});
|
||||
list = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/user-groups', { params: options });
|
||||
return res.data as Paginated<UserGroupWithUserCount>;
|
||||
}
|
||||
};
|
||||
|
||||
async get(id: string) {
|
||||
get = async (id: string) => {
|
||||
const res = await this.api.get(`/user-groups/${id}`);
|
||||
return res.data as UserGroupWithUsers;
|
||||
}
|
||||
};
|
||||
|
||||
async create(user: UserGroupCreate) {
|
||||
create = async (user: UserGroupCreate) => {
|
||||
const res = await this.api.post('/user-groups', user);
|
||||
return res.data as UserGroupWithUsers;
|
||||
}
|
||||
};
|
||||
|
||||
async update(id: string, user: UserGroupCreate) {
|
||||
update = async (id: string, user: UserGroupCreate) => {
|
||||
const res = await this.api.put(`/user-groups/${id}`, user);
|
||||
return res.data as UserGroupWithUsers;
|
||||
}
|
||||
};
|
||||
|
||||
async remove(id: string) {
|
||||
remove = async (id: string) => {
|
||||
await this.api.delete(`/user-groups/${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
async updateUsers(id: string, userIds: string[]) {
|
||||
updateUsers = async (id: string, userIds: string[]) => {
|
||||
const res = await this.api.put(`/user-groups/${id}/users`, { userIds });
|
||||
return res.data as UserGroupWithUsers;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import type { SignupTokenDto } from '$lib/types/signup-token.type';
|
||||
import type { UserGroup } from '$lib/types/user-group.type';
|
||||
import type { User, UserCreate, UserSignUp } from '$lib/types/user.type';
|
||||
@@ -8,125 +8,113 @@ import { get } from 'svelte/store';
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class UserService extends APIService {
|
||||
async list(options?: SearchPaginationSortRequest) {
|
||||
const res = await this.api.get('/users', {
|
||||
params: options
|
||||
});
|
||||
list = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/users', { params: options });
|
||||
return res.data as Paginated<User>;
|
||||
}
|
||||
};
|
||||
|
||||
async get(id: string) {
|
||||
get = async (id: string) => {
|
||||
const res = await this.api.get(`/users/${id}`);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async getCurrent() {
|
||||
getCurrent = async () => {
|
||||
const res = await this.api.get('/users/me');
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async create(user: UserCreate) {
|
||||
create = async (user: UserCreate) => {
|
||||
const res = await this.api.post('/users', user);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async getUserGroups(userId: string) {
|
||||
getUserGroups = async (userId: string) => {
|
||||
const res = await this.api.get(`/users/${userId}/groups`);
|
||||
return res.data as UserGroup[];
|
||||
}
|
||||
};
|
||||
|
||||
async update(id: string, user: UserCreate) {
|
||||
update = async (id: string, user: UserCreate) => {
|
||||
const res = await this.api.put(`/users/${id}`, user);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async updateCurrent(user: UserCreate) {
|
||||
updateCurrent = async (user: UserCreate) => {
|
||||
const res = await this.api.put('/users/me', user);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async remove(id: string) {
|
||||
remove = async (id: string) => {
|
||||
await this.api.delete(`/users/${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
async updateProfilePicture(userId: string, image: File) {
|
||||
updateProfilePicture = async (userId: string, image: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', image!);
|
||||
|
||||
await this.api.put(`/users/${userId}/profile-picture`, formData);
|
||||
cachedProfilePicture.bustCache(userId);
|
||||
}
|
||||
};
|
||||
|
||||
async updateCurrentUsersProfilePicture(image: File) {
|
||||
updateCurrentUsersProfilePicture = async (image: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', image!);
|
||||
|
||||
await this.api.put('/users/me/profile-picture', formData);
|
||||
cachedProfilePicture.bustCache(get(userStore)!.id);
|
||||
}
|
||||
};
|
||||
|
||||
async resetCurrentUserProfilePicture() {
|
||||
resetCurrentUserProfilePicture = async () => {
|
||||
await this.api.delete(`/users/me/profile-picture`);
|
||||
cachedProfilePicture.bustCache(get(userStore)!.id);
|
||||
}
|
||||
};
|
||||
|
||||
async resetProfilePicture(userId: string) {
|
||||
resetProfilePicture = async (userId: string) => {
|
||||
await this.api.delete(`/users/${userId}/profile-picture`);
|
||||
cachedProfilePicture.bustCache(userId);
|
||||
}
|
||||
};
|
||||
|
||||
async createOneTimeAccessToken(userId: string = 'me', ttl?: string|number) {
|
||||
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
||||
userId,
|
||||
ttl,
|
||||
});
|
||||
createOneTimeAccessToken = async (userId: string = 'me', ttl?: string | number) => {
|
||||
const res = await this.api.post(`/users/${userId}/one-time-access-token`, { userId, ttl });
|
||||
return res.data.token;
|
||||
}
|
||||
};
|
||||
|
||||
async createSignupToken(ttl: string|number, usageLimit: number) {
|
||||
const res = await this.api.post(`/signup-tokens`, {
|
||||
ttl,
|
||||
usageLimit
|
||||
});
|
||||
createSignupToken = async (ttl: string | number, usageLimit: number) => {
|
||||
const res = await this.api.post(`/signup-tokens`, { ttl, usageLimit });
|
||||
return res.data.token;
|
||||
}
|
||||
};
|
||||
|
||||
async exchangeOneTimeAccessToken(token: string) {
|
||||
exchangeOneTimeAccessToken = async (token: string) => {
|
||||
const res = await this.api.post(`/one-time-access-token/${token}`);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async requestOneTimeAccessEmailAsUnauthenticatedUser(email: string, redirectPath?: string) {
|
||||
requestOneTimeAccessEmailAsUnauthenticatedUser = async (email: string, redirectPath?: string) => {
|
||||
await this.api.post('/one-time-access-email', { email, redirectPath });
|
||||
}
|
||||
};
|
||||
|
||||
async requestOneTimeAccessEmailAsAdmin(userId: string, ttl: string|number) {
|
||||
requestOneTimeAccessEmailAsAdmin = async (userId: string, ttl: string | number) => {
|
||||
await this.api.post(`/users/${userId}/one-time-access-email`, { ttl });
|
||||
}
|
||||
};
|
||||
|
||||
async updateUserGroups(id: string, userGroupIds: string[]) {
|
||||
updateUserGroups = async (id: string, userGroupIds: string[]) => {
|
||||
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async signup(data: UserSignUp) {
|
||||
signup = async (data: UserSignUp) => {
|
||||
const res = await this.api.post(`/signup`, data);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async signupInitialUser(data: UserSignUp) {
|
||||
signupInitialUser = async (data: UserSignUp) => {
|
||||
const res = await this.api.post(`/signup/setup`, data);
|
||||
return res.data as User;
|
||||
}
|
||||
};
|
||||
|
||||
async listSignupTokens(options?: SearchPaginationSortRequest) {
|
||||
const res = await this.api.get('/signup-tokens', {
|
||||
params: options
|
||||
});
|
||||
listSignupTokens = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/signup-tokens', { params: options });
|
||||
return res.data as Paginated<SignupTokenDto>;
|
||||
}
|
||||
};
|
||||
|
||||
async deleteSignupToken(tokenId: string) {
|
||||
deleteSignupToken = async (tokenId: string) => {
|
||||
await this.api.delete(`/signup-tokens/${tokenId}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { version as currentVersion } from '$app/environment';
|
||||
import axios from 'axios';
|
||||
import APIService from './api-service';
|
||||
|
||||
async function getNewestVersion() {
|
||||
const response = await axios
|
||||
.get('/api/version/latest', {
|
||||
timeout: 2000
|
||||
})
|
||||
.then((res) => res.data);
|
||||
export default class VersionService extends APIService {
|
||||
getNewestVersion = async () => {
|
||||
const response = await this.api
|
||||
.get('/version/latest', { timeout: 2000 })
|
||||
.then((res) => res.data);
|
||||
return response.latestVersion;
|
||||
};
|
||||
|
||||
return response.latestVersion;
|
||||
getCurrentVersion = () => currentVersion;
|
||||
}
|
||||
|
||||
function getCurrentVersion() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
export default {
|
||||
getNewestVersion,
|
||||
getCurrentVersion,
|
||||
};
|
||||
|
||||
@@ -3,45 +3,36 @@ import type { User } from '$lib/types/user.type';
|
||||
import APIService from './api-service';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/browser';
|
||||
|
||||
class WebAuthnService extends APIService {
|
||||
async getRegistrationOptions() {
|
||||
return (await this.api.get(`/webauthn/register/start`)).data;
|
||||
}
|
||||
getRegistrationOptions = async () => (await this.api.get(`/webauthn/register/start`)).data;
|
||||
|
||||
async finishRegistration(body: RegistrationResponseJSON) {
|
||||
return (await this.api.post(`/webauthn/register/finish`, body)).data as Passkey;
|
||||
}
|
||||
finishRegistration = async (body: RegistrationResponseJSON) =>
|
||||
(await this.api.post(`/webauthn/register/finish`, body)).data as Passkey;
|
||||
|
||||
async getLoginOptions() {
|
||||
return (await this.api.get(`/webauthn/login/start`)).data;
|
||||
}
|
||||
getLoginOptions = async () => (await this.api.get(`/webauthn/login/start`)).data;
|
||||
|
||||
async finishLogin(body: AuthenticationResponseJSON) {
|
||||
return (await this.api.post(`/webauthn/login/finish`, body)).data as User;
|
||||
}
|
||||
finishLogin = async (body: AuthenticationResponseJSON) =>
|
||||
(await this.api.post(`/webauthn/login/finish`, body)).data as User;
|
||||
|
||||
async logout() {
|
||||
await this.api.post(`/webauthn/logout`);
|
||||
userStore.clearUser();
|
||||
}
|
||||
logout = async () => {
|
||||
await this.api.post(`/webauthn/logout`);
|
||||
userStore.clearUser();
|
||||
};
|
||||
|
||||
async listCredentials() {
|
||||
return (await this.api.get(`/webauthn/credentials`)).data as Passkey[];
|
||||
}
|
||||
listCredentials = async () => (await this.api.get(`/webauthn/credentials`)).data as Passkey[];
|
||||
|
||||
async removeCredential(id: string) {
|
||||
await this.api.delete(`/webauthn/credentials/${id}`);
|
||||
}
|
||||
removeCredential = async (id: string) => {
|
||||
await this.api.delete(`/webauthn/credentials/${id}`);
|
||||
};
|
||||
|
||||
async updateCredentialName(id: string, name: string) {
|
||||
await this.api.patch(`/webauthn/credentials/${id}`, { name });
|
||||
}
|
||||
updateCredentialName = async (id: string, name: string) => {
|
||||
await this.api.patch(`/webauthn/credentials/${id}`, { name });
|
||||
};
|
||||
|
||||
async reauthenticate(body?: AuthenticationResponseJSON) {
|
||||
const res = await this.api.post('/webauthn/reauthenticate', body);
|
||||
return res.data.reauthenticationToken as string;
|
||||
}
|
||||
reauthenticate = async (body?: AuthenticationResponseJSON) => {
|
||||
const res = await this.api.post('/webauthn/reauthenticate', body);
|
||||
return res.data.reauthenticationToken as string;
|
||||
};
|
||||
}
|
||||
|
||||
export default WebAuthnService;
|
||||
|
||||
26
frontend/src/lib/types/advanced-table.type.ts
Normal file
26
frontend/src/lib/types/advanced-table.type.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
|
||||
export type AdvancedTableColumn<T extends Record<string, any>> = {
|
||||
label: string;
|
||||
column?: keyof T & string;
|
||||
key?: string;
|
||||
value?: (item: T) => string | number | boolean | undefined;
|
||||
cell?: Snippet<[{ item: T }]>;
|
||||
sortable?: boolean;
|
||||
filterableValues?: {
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
icon?: Component;
|
||||
}[];
|
||||
hidden?: boolean;
|
||||
};
|
||||
export type CreateAdvancedTableActions<T extends Record<string, any>> = (item: T) => AdvancedTableAction<T>[];
|
||||
|
||||
export type AdvancedTableAction<T> = {
|
||||
label: string;
|
||||
icon?: Component;
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost';
|
||||
onClick: (item: T) => void;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -8,13 +8,11 @@ export type SortRequest = {
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export type FilterMap = Record<string, string>;
|
||||
|
||||
export type SearchPaginationSortRequest = {
|
||||
export type ListRequestOptions = {
|
||||
search?: string;
|
||||
pagination?: PaginationRequest;
|
||||
sort?: SortRequest;
|
||||
filters?: FilterMap;
|
||||
filters?: Record<string, (string | boolean)[]>;
|
||||
};
|
||||
|
||||
export type PaginationResponse = {
|
||||
@@ -4,6 +4,7 @@ export type OidcClientMetaData = {
|
||||
id: string;
|
||||
name: string;
|
||||
hasLogo: boolean;
|
||||
hasDarkLogo: boolean;
|
||||
requiresReauthentication: boolean;
|
||||
launchURL?: string;
|
||||
};
|
||||
@@ -37,17 +38,20 @@ export type OidcClientWithAllowedUserGroupsCount = OidcClient & {
|
||||
allowedUserGroupsCount: number;
|
||||
};
|
||||
|
||||
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
||||
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo' | 'hasDarkLogo'>;
|
||||
export type OidcClientCreate = OidcClientUpdate & {
|
||||
id?: string;
|
||||
};
|
||||
export type OidcClientUpdateWithLogo = OidcClientUpdate & {
|
||||
logo: File | null | undefined;
|
||||
darkLogo: File | null | undefined;
|
||||
};
|
||||
|
||||
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
||||
logo?: File | null;
|
||||
logoUrl?: string;
|
||||
darkLogo?: File | null;
|
||||
darkLogoUrl?: string;
|
||||
};
|
||||
|
||||
export type OidcDeviceCodeInfo = {
|
||||
|
||||
@@ -9,73 +9,85 @@ type CachableImage = {
|
||||
|
||||
export const cachedApplicationLogo: CachableImage = {
|
||||
getUrl: (light = true) => {
|
||||
let url = '/api/application-images/logo';
|
||||
if (!light) {
|
||||
url += '?light=false';
|
||||
}
|
||||
const url = new URL('/api/application-images/logo', window.location.origin);
|
||||
if (!light) url.searchParams.set('light', 'false');
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (light = true) => {
|
||||
let url = '/api/application-images/logo';
|
||||
if (!light) {
|
||||
url += '?light=false';
|
||||
}
|
||||
const url = new URL('/api/application-images/logo', window.location.origin);
|
||||
if (!light) url.searchParams.set('light', 'false');
|
||||
bustImageCache(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const cachedBackgroundImage: CachableImage = {
|
||||
getUrl: () => getCachedImageUrl('/api/application-images/background'),
|
||||
bustCache: () => bustImageCache('/api/application-images/background')
|
||||
getUrl: () =>
|
||||
getCachedImageUrl(new URL('/api/application-images/background', window.location.origin)),
|
||||
bustCache: () =>
|
||||
bustImageCache(new URL('/api/application-images/background', window.location.origin))
|
||||
};
|
||||
|
||||
export const cachedProfilePicture: CachableImage = {
|
||||
getUrl: (userId: string) => {
|
||||
const url = `/api/users/${userId}/profile-picture.png`;
|
||||
const url = new URL(`/api/users/${userId}/profile-picture.png`, window.location.origin);
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (userId: string) => {
|
||||
const url = `/api/users/${userId}/profile-picture.png`;
|
||||
const url = new URL(`/api/users/${userId}/profile-picture.png`, window.location.origin);
|
||||
bustImageCache(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const cachedOidcClientLogo: CachableImage = {
|
||||
getUrl: (clientId: string) => {
|
||||
const url = `/api/oidc/clients/${clientId}/logo`;
|
||||
getUrl: (clientId: string, light = true) => {
|
||||
const url = new URL(`/api/oidc/clients/${clientId}/logo`, window.location.origin);
|
||||
if (!light) url.searchParams.set('light', 'false');
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (clientId: string) => {
|
||||
const url = `/api/oidc/clients/${clientId}/logo`;
|
||||
bustCache: (clientId: string, light = true) => {
|
||||
const url = new URL(`/api/oidc/clients/${clientId}/logo`, window.location.origin);
|
||||
if (!light) url.searchParams.set('light', 'false');
|
||||
bustImageCache(url);
|
||||
}
|
||||
};
|
||||
|
||||
function getCachedImageUrl(url: string) {
|
||||
const skipCacheUntil = getSkipCacheUntil(url);
|
||||
function getCachedImageUrl(url: URL) {
|
||||
const baseKey = normalizeUrlForKey(url);
|
||||
const skipCacheUntil = getSkipCacheUntil(baseKey);
|
||||
const skipCache = skipCacheUntil > Date.now();
|
||||
|
||||
const finalUrl = new URL(url.toString());
|
||||
if (skipCache) {
|
||||
const skipCacheParam = new URLSearchParams();
|
||||
skipCacheParam.append('skip-cache', skipCacheUntil.toString());
|
||||
url += '?' + skipCacheParam.toString();
|
||||
finalUrl.searchParams.set('skip-cache', skipCacheUntil.toString());
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
return finalUrl.pathname + (finalUrl.search ? `?${finalUrl.searchParams.toString()}` : '');
|
||||
}
|
||||
|
||||
function bustImageCache(url: string) {
|
||||
const skipCacheUntil: SkipCacheUntil = JSON.parse(
|
||||
localStorage.getItem('skip-cache-until') ?? '{}'
|
||||
);
|
||||
skipCacheUntil[hashKey(url)] = Date.now() + 1000 * 60 * 15; // 15 minutes
|
||||
localStorage.setItem('skip-cache-until', JSON.stringify(skipCacheUntil));
|
||||
function bustImageCache(url: URL) {
|
||||
const key = normalizeUrlForKey(url);
|
||||
const expiresAt = Date.now() + 1000 * 60 * 15;
|
||||
|
||||
const store: SkipCacheUntil = JSON.parse(localStorage.getItem('skip-cache-until') ?? '{}');
|
||||
store[key] = expiresAt;
|
||||
localStorage.setItem('skip-cache-until', JSON.stringify(store));
|
||||
}
|
||||
|
||||
function getSkipCacheUntil(url: string) {
|
||||
const skipCacheUntil: SkipCacheUntil = JSON.parse(
|
||||
localStorage.getItem('skip-cache-until') ?? '{}'
|
||||
function getSkipCacheUntil(key: string): number {
|
||||
const store: SkipCacheUntil = JSON.parse(localStorage.getItem('skip-cache-until') ?? '{}');
|
||||
return store[key] ?? 0;
|
||||
}
|
||||
|
||||
// Removes transient params and normalizes query order before hashing
|
||||
function normalizeUrlForKey(url: URL) {
|
||||
const u = new URL(url.toString());
|
||||
u.searchParams.delete('skip-cache');
|
||||
|
||||
const sortedParams = new URLSearchParams(
|
||||
[...u.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b))
|
||||
);
|
||||
return skipCacheUntil[hashKey(url)] ?? 0;
|
||||
const normalized = u.pathname + (sortedParams.toString() ? `?${sortedParams.toString()}` : '');
|
||||
return hashKey(normalized);
|
||||
}
|
||||
|
||||
function hashKey(key: string): string {
|
||||
@@ -83,7 +95,7 @@ function hashKey(key: string): string {
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
const char = key.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
hash |= 0;
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export function getAuthRedirectPath(path: string, user: User | null) {
|
||||
path.startsWith('/lc/') ||
|
||||
path == '/signup' ||
|
||||
path == '/signup/setup' ||
|
||||
path == '/setup' ||
|
||||
path.startsWith('/st/');
|
||||
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
|
||||
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import ConfirmDialog from '$lib/components/confirm-dialog/confirm-dialog.svelte';
|
||||
import Error from '$lib/components/error.svelte';
|
||||
import Header from '$lib/components/header/header.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { getAuthRedirectPath } from '$lib/utils/redirection-util';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import type { Snippet } from 'svelte';
|
||||
import '../app.css';
|
||||
@@ -20,12 +17,7 @@
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
const { user, appConfig } = data;
|
||||
|
||||
const redirectPath = getAuthRedirectPath(page.url.pathname, user);
|
||||
if (redirectPath) {
|
||||
goto(redirectPath);
|
||||
}
|
||||
const { appConfig } = data;
|
||||
</script>
|
||||
|
||||
{#if !appConfig}
|
||||
|
||||
@@ -3,11 +3,13 @@ import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { setLocaleForLibraries } from '$lib/utils/locale.util';
|
||||
import { getAuthRedirectPath } from '$lib/utils/redirection-util';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
export const load: LayoutLoad = async ({ url }) => {
|
||||
const userService = new UserService();
|
||||
const appConfigService = new AppConfigService();
|
||||
|
||||
@@ -22,6 +24,11 @@ export const load: LayoutLoad = async () => {
|
||||
|
||||
const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]);
|
||||
|
||||
const redirectPath = getAuthRedirectPath(url.pathname, user);
|
||||
if (redirectPath) {
|
||||
redirect(302, redirectPath);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
await userStore.setUser(user);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { OidcClientMetaData } from '$lib/types/oidc.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import { mode } from 'mode-watcher';
|
||||
|
||||
const {
|
||||
success,
|
||||
@@ -28,6 +29,8 @@
|
||||
animationDone = false;
|
||||
}
|
||||
});
|
||||
|
||||
const isLightMode = $derived(mode.current === 'light');
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center gap-3">
|
||||
@@ -60,8 +63,8 @@
|
||||
</div>
|
||||
{:else if client.hasLogo}
|
||||
<img
|
||||
class="size-10 aspect-square object-contain"
|
||||
src={cachedOidcClientLogo.getUrl(client.id)}
|
||||
class="aspect-square size-10 object-contain"
|
||||
src={cachedOidcClientLogo.getUrl(client.id, isLightMode)}
|
||||
draggable={false}
|
||||
alt={m.client_logo()}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { OidcDeviceCodeInfo } from '$lib/types/oidc.type';
|
||||
import { getAxiosErrorMessage } from '$lib/utils/error-util';
|
||||
@@ -72,10 +71,7 @@
|
||||
<title>{m.authorize_device()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper
|
||||
animate={!$appConfigStore.disableAnimations}
|
||||
showAlternativeSignInMethodButton={$userStore == null}
|
||||
>
|
||||
<SignInWrapper showAlternativeSignInMethodButton={$userStore == null}>
|
||||
<div class="flex justify-center">
|
||||
{#if deviceInfo?.client}
|
||||
<ClientProviderImages client={deviceInfo.client} {success} error={!!errorMessage} />
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<title>{m.sign_in()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper animate={!$appConfigStore.disableAnimations} showAlternativeSignInMethodButton>
|
||||
<SignInWrapper showAlternativeSignInMethodButton>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<title>{m.logout()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-muted rounded-2xl p-3">
|
||||
<Logo class="size-10" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user