Compare commits

..

23 Commits

Author SHA1 Message Date
Alessandro (Ale) Segala
419b7613ee Update example .env to use database for keys storage
Given recent conversations on how the database will be the preferred (or sole) method in a future release, start by updating the example env
2025-11-10 07:17:38 -08:00
Elias Schneider
42b872d6b2 chore(translations): update translations via Crowdin (#1085) 2025-11-10 14:46:48 +01:00
Elias Schneider
bfd71d090c feat: add support for S3 storage backend (#1080)
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
2025-11-10 09:02:25 +00:00
Kyle Mendell
d5e0cfd4a6 feat: light/dark/system mode switcher (#1081) 2025-11-09 13:28:58 -06:00
Kyle Mendell
9981304b4b chore(deps): update pnpm to 10.20 (#1082) 2025-11-08 13:09:05 -06:00
Elias Schneider
5cf73e9309 fix: use quoted-printable encoding for mails to prevent line limitation 2025-11-08 17:34:43 +01:00
Elias Schneider
f125cf0dad release: 1.15.0 2025-11-06 15:49:39 +01:00
Elias Schneider
6a038fcf9a fix: remove redundant indexes in Postgres 2025-11-06 15:02:39 +01:00
Elias Schneider
76e0192cee fix: disabled property gets ignored when creating an user 2025-11-06 12:28:07 +01:00
Elias Schneider
3ebf94dd84 chore(translations): update translations via Crowdin (#1059) 2025-11-05 11:28:14 +01:00
dai
7ec57437ac fix: replace %lang% placeholder in html lang (#1071)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-11-04 13:01:12 +00:00
Elias Schneider
ed2c7b2303 feat: add ability to set default profile picture (#1061)
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
2025-11-04 13:40:00 +01:00
Elias Schneider
e03270eb9d fix: sorting by PKCE and re-auth of OIDC clients 2025-11-04 13:27:05 +01:00
Elias Schneider
d683d18d91 chore: add support for OpenBSD binaries 2025-11-01 16:26:43 +01:00
Elias Schneider
f184120890 feat: open edit page on table row click 2025-10-29 10:46:03 +01:00
Elias Schneider
04d8500910 release: 1.14.2 2025-10-29 09:25:28 +01:00
Mufeed Ali
93639dddb2 fix: dark oidc client icons not saved on client creation (#1057) 2025-10-28 12:35:56 +01:00
Elias Schneider
a190529117 chore(translations): add Turkish language files 2025-10-28 09:31:32 +01:00
Elias Schneider
73392b5837 release: 1.14.1 2025-10-27 14:07:34 +01:00
Elias Schneider
65616f65e5 fix: ignore trailing slashes in APP_URL 2025-10-27 10:48:27 +01:00
Elias Schneider
98a99fbb0a chore(translations): update translations via Crowdin (#1048) 2025-10-27 09:48:39 +01:00
Quentin L'Hours
3f3b6b88fd fix: use credProps to save passkey on firefox android (#1055) 2025-10-27 09:48:24 +01:00
Mufeed Ali
8f98d8c0b4 fix: Prevent blinding FOUC in dark mode (#1054) 2025-10-26 20:40:25 +01:00
90 changed files with 2312 additions and 972 deletions

View File

@@ -4,3 +4,11 @@ TRUST_PROXY=false
MAXMIND_LICENSE_KEY= MAXMIND_LICENSE_KEY=
PUID=1000 PUID=1000
PGID=1000 PGID=1000
KEYS_STORAGE=database
# Encryption key used to protect sensitive data
# Can be generated with `openssl rand -base64 32`
# See https://pocket-id.org/docs/configuration/environment-variables#encryption-keys
ENCRYPTION_KEY=
# Alternatively, point to a file containing the key (could be a Docker secret)
#ENCRYPTION_KEY_FILE=

View File

@@ -57,7 +57,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
db: [sqlite, postgres] db: [sqlite, postgres, sqlite-s3]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@@ -75,6 +76,7 @@ jobs:
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
- name: Cache PostgreSQL Docker image - name: Cache PostgreSQL Docker image
if: matrix.db == 'postgres' if: matrix.db == 'postgres'
uses: actions/cache@v3 uses: actions/cache@v3
@@ -89,9 +91,10 @@ jobs:
docker pull postgres:17 docker pull postgres:17
docker save postgres:17 > /tmp/postgres-image.tar docker save postgres:17 > /tmp/postgres-image.tar
- name: Load PostgreSQL image from cache - name: Load PostgreSQL image
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit == 'true' if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/postgres-image.tar run: docker load < /tmp/postgres-image.tar
- name: Cache LLDAP Docker image - name: Cache LLDAP Docker image
uses: actions/cache@v3 uses: actions/cache@v3
id: lldap-cache id: lldap-cache
@@ -105,9 +108,28 @@ jobs:
docker pull nitnelave/lldap:stable docker pull nitnelave/lldap:stable
docker save nitnelave/lldap:stable > /tmp/lldap-image.tar docker save nitnelave/lldap:stable > /tmp/lldap-image.tar
- name: Load LLDAP image from cache - name: Load LLDAP image
if: steps.lldap-cache.outputs.cache-hit == 'true' if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar run: docker load < /tmp/lldap-image.tar
- name: Cache Localstack S3 Docker image
if: matrix.db == 'sqlite-s3'
uses: actions/cache@v3
id: s3-cache
with:
path: /tmp/localstack-s3-image.tar
key: localstack-s3-latest-${{ runner.os }}
- name: Pull and save Localstack S3 image
if: matrix.db == 'sqlite-s3' && steps.s3-cache.outputs.cache-hit != 'true'
run: |
docker pull localstack/localstack:s3-latest
docker save localstack/localstack:s3-latest > /tmp/localstack-s3-image.tar
- name: Load Localstack S3 image
if: matrix.db == 'sqlite-s3' && steps.s3-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/localstack-s3-image.tar
- name: Download Docker image artifact - name: Download Docker image artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -124,6 +146,7 @@ jobs:
working-directory: ./tests working-directory: ./tests
if: steps.playwright-cache.outputs.cache-hit != 'true' if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium run: pnpm exec playwright install --with-deps chromium
- name: Run Docker Container (sqlite) with LDAP - name: Run Docker Container (sqlite) with LDAP
if: matrix.db == 'sqlite' if: matrix.db == 'sqlite'
working-directory: ./tests/setup working-directory: ./tests/setup
@@ -138,6 +161,13 @@ jobs:
docker compose -f docker-compose-postgres.yml up -d docker compose -f docker-compose-postgres.yml up -d
docker compose -f docker-compose-postgres.yml logs -f pocket-id &> /tmp/backend.log & docker compose -f docker-compose-postgres.yml logs -f pocket-id &> /tmp/backend.log &
- name: Run Docker Container (sqlite-s3) with LDAP + S3
if: matrix.db == 'sqlite-s3'
working-directory: ./tests/setup
run: |
docker compose -f docker-compose-s3.yml up -d
docker compose -f docker-compose-s3.yml logs -f pocket-id &> /tmp/backend.log &
- name: Run Playwright tests - name: Run Playwright tests
working-directory: ./tests working-directory: ./tests
run: pnpm exec playwright test run: pnpm exec playwright test

5
.gitignore vendored
View File

@@ -1,8 +1,12 @@
# JetBrains # JetBrains
**/.idea **/.idea
# Node
node_modules node_modules
# PNPM
.pnpm-store/
# Output # Output
.output .output
.vercel .vercel
@@ -21,6 +25,7 @@ Thumbs.db
.env.* .env.*
!.env.development-example !.env.development-example
!.env.test !.env.test
!.env.example
# Vite # Vite
vite.config.js.timestamp-* vite.config.js.timestamp-*

View File

@@ -1 +1 @@
1.14.0 1.15.0

View File

@@ -1,3 +1,45 @@
## v1.15.0
### Bug Fixes
- sorting by PKCE and re-auth of OIDC clients ([e03270e](https://github.com/pocket-id/pocket-id/commit/e03270eb9d474735ff4a1b4d8c90f1857b8cd52b) by @stonith404)
- replace %lang% placeholder in html lang ([#1071](https://github.com/pocket-id/pocket-id/pull/1071) by @daimond113)
- disabled property gets ignored when creating an user ([76e0192](https://github.com/pocket-id/pocket-id/commit/76e0192ceec339b6ddb4ad3424057d2bb48fae8f) by @stonith404)
- remove redundant indexes in Postgres ([6a038fc](https://github.com/pocket-id/pocket-id/commit/6a038fcf9afabbf00c45e42071e9bbe62ecab403) by @stonith404)
### Features
- open edit page on table row click ([f184120](https://github.com/pocket-id/pocket-id/commit/f184120890c32f1e75a918c171084878a10e8b42) by @stonith404)
- add ability to set default profile picture ([#1061](https://github.com/pocket-id/pocket-id/pull/1061) by @stonith404)
### Other
- add support for OpenBSD binaries ([d683d18](https://github.com/pocket-id/pocket-id/commit/d683d18d9109ca2850e278b78f7bf3e5aca1d34d) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v1.14.2...v1.15.0
## 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 ## v1.14.0
### Bug Fixes ### Bug Fixes

View File

@@ -32,10 +32,6 @@ func init() {
panic(fmt.Errorf("failed to read index.html: %w", iErr)) 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) { writeIndexFn = func(w io.Writer, nonce string) (err error) {
// If there's no nonce, write the index as-is // If there's no nonce, write the index as-is
if nonce == "" { if nonce == "" {
@@ -43,23 +39,16 @@ func init() {
return err return err
} }
// We have a nonce, so first write the index until the <script> tag // Add nonce to all <script> tags
// Then we write the modified script tag // We replace "<script" with `<script nonce="..."` everywhere it appears
// Finally, the rest of the index modified := bytes.ReplaceAll(
_, err = w.Write(index[0:idx]) index,
if err != nil { []byte(scriptTag),
return err []byte(`<script nonce="`+nonce+`">`),
} )
_, err = w.Write([]byte(`<script nonce="` + nonce + `">`))
if err != nil {
return err
}
_, err = w.Write(index[(idx + len(scriptTag)):])
if err != nil {
return err
}
return nil _, err = w.Write(modified)
return err
} }
} }

View File

@@ -3,6 +3,11 @@ module github.com/pocket-id/pocket-id/backend
go 1.25 go 1.25
require ( require (
github.com/aws/aws-sdk-go-v2 v1.39.6
github.com/aws/aws-sdk-go-v2/config v1.31.17
github.com/aws/aws-sdk-go-v2/credentials v1.18.21
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
github.com/aws/smithy-go v1.23.2
github.com/caarlos0/env/v11 v11.3.1 github.com/caarlos0/env/v11 v11.3.1
github.com/cenkalti/backoff/v5 v5.0.3 github.com/cenkalti/backoff/v5 v5.0.3
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -54,11 +59,23 @@ require (
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
@@ -80,7 +97,6 @@ require (
github.com/google/go-github/v39 v39.2.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-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.6 // 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -139,8 +155,6 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.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/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.8.0 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect

View File

@@ -4,39 +4,72 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 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 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 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 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.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/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= 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 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
@@ -46,53 +79,38 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 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/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 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-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= 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 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ= 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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4=
github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY= 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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 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 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 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 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 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 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 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 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-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 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= 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 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= 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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -106,16 +124,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 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 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k= 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 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY= 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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -124,12 +136,8 @@ 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/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 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= 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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -138,15 +146,12 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 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 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -154,12 +159,6 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= 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.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -167,7 +166,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -176,8 +174,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
@@ -226,12 +222,8 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/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 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 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 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= 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 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= 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 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
@@ -244,8 +236,6 @@ github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
@@ -265,8 +255,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -275,8 +263,6 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU= github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= 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 h1:Gyljxck1kHbBxDgLM++NfDWBqvu1pWWfT8XbosSo0bo=
github.com/oschwald/maxminddb-golang/v2 v2.0.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0= 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 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -285,22 +271,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 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 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 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 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= 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 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= 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 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
@@ -311,20 +289,13 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 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 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 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.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -332,14 +303,11 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@@ -350,281 +318,137 @@ github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXV
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 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 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4= 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 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E= 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 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g= 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 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/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 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= 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/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 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 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= 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 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/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 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/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 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/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 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= 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 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/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 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/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 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= 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 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= 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 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/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 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= 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 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= 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 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 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 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 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 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= go.opentelemetry.io/otel/sdk/log 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/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 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/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 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 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 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 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 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 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-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-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.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 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 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 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 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.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 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= 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=
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 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 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-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=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.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 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 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.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 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.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 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.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 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.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 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.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 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/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 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= 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 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= 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 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= 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/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 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= 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/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
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/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY= modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 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 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 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 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@@ -635,8 +459,6 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 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 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= 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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=

View File

@@ -2,68 +2,76 @@ package bootstrap
import ( import (
"bytes" "bytes"
"context"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io"
"io/fs"
"log/slog" "log/slog"
"os" "os"
"path" "path"
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/resources" "github.com/pocket-id/pocket-id/backend/resources"
) )
// initApplicationImages copies the images from the images directory to the application-images directory // initApplicationImages copies the images from the embedded directory to the storage backend
// and returns a map containing the detected file extensions in the application-images directory. // and returns a map containing the detected file extensions in the application-images directory.
func initApplicationImages() (map[string]string, error) { func initApplicationImages(ctx context.Context, fileStorage storage.FileStorage) (map[string]string, error) {
// Previous versions of images // Previous versions of images
// If these are found, they are deleted // If these are found, they are deleted
legacyImageHashes := imageHashMap{ legacyImageHashes := imageHashMap{
"background.jpg": mustDecodeHex("138d510030ed845d1d74de34658acabff562d306476454369a60ab8ade31933f"), "background.jpg": mustDecodeHex("138d510030ed845d1d74de34658acabff562d306476454369a60ab8ade31933f"),
} }
dirPath := common.EnvConfig.UploadPath + "/application-images"
sourceFiles, err := resources.FS.ReadDir("images") sourceFiles, err := resources.FS.ReadDir("images")
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to read directory: %w", err) return nil, fmt.Errorf("failed to read directory: %w", err)
} }
destinationFiles, err := os.ReadDir(dirPath) destinationFiles, err := fileStorage.List(ctx, "application-images")
if err != nil && !os.IsNotExist(err) { if err != nil {
return nil, fmt.Errorf("failed to read directory: %w", err) if storage.IsNotExist(err) {
destinationFiles = []storage.ObjectInfo{}
} else {
return nil, fmt.Errorf("failed to list application images: %w", err)
}
} }
dstNameToExt := make(map[string]string, len(destinationFiles)) dstNameToExt := make(map[string]string, len(destinationFiles))
for _, f := range destinationFiles { for _, f := range destinationFiles {
if f.IsDir() {
continue
}
name := f.Name()
nameWithoutExt, ext := utils.SplitFileName(name)
destFilePath := path.Join(dirPath, name)
// Skip directories // Skip directories
if f.IsDir() { _, name := path.Split(f.Path)
if name == "" {
continue continue
} }
nameWithoutExt, ext := utils.SplitFileName(name)
h, err := utils.CreateSha256FileHash(destFilePath) reader, _, err := fileStorage.Open(ctx, f.Path)
if err != nil { if err != nil {
slog.Warn("Failed to get hash for file", slog.String("name", name), slog.Any("error", err)) if errors.Is(err, fs.ErrNotExist) {
continue
}
slog.Warn("Failed to open application image for hashing", slog.String("name", name), slog.Any("error", err))
continue
}
hash, err := hashStream(reader)
reader.Close()
if err != nil {
slog.Warn("Failed to hash application image", slog.String("name", name), slog.Any("error", err))
continue continue
} }
// Check if the file is a legacy one - if so, delete it // Check if the file is a legacy one - if so, delete it
if legacyImageHashes.Contains(h) { if legacyImageHashes.Contains(hash) {
slog.Info("Found legacy application image that will be removed", slog.String("name", name)) slog.Info("Found legacy application image that will be removed", slog.String("name", name))
err = os.Remove(destFilePath) if err := fileStorage.Delete(ctx, f.Path); err != nil {
if err != nil {
return nil, fmt.Errorf("failed to remove legacy file '%s': %w", name, err) return nil, fmt.Errorf("failed to remove legacy file '%s': %w", name, err)
} }
continue continue
} }
// Track existing files
dstNameToExt[nameWithoutExt] = ext dstNameToExt[nameWithoutExt] = ext
} }
@@ -76,21 +84,21 @@ func initApplicationImages() (map[string]string, error) {
name := sourceFile.Name() name := sourceFile.Name()
nameWithoutExt, ext := utils.SplitFileName(name) nameWithoutExt, ext := utils.SplitFileName(name)
srcFilePath := path.Join("images", name) srcFilePath := path.Join("images", name)
destFilePath := path.Join(dirPath, name)
// Skip if there's already an image at the path
// We do not check the extension because users could have uploaded a different one
if _, exists := dstNameToExt[nameWithoutExt]; exists { if _, exists := dstNameToExt[nameWithoutExt]; exists {
continue continue
} }
slog.Info("Writing new application image", slog.String("name", name)) slog.Info("Writing new application image", slog.String("name", name))
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath) srcFile, err := resources.FS.Open(srcFilePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to copy file: %w", err) return nil, fmt.Errorf("failed to open embedded file '%s': %w", name, err)
} }
if err := fileStorage.Save(ctx, path.Join("application-images", name), srcFile); err != nil {
// Track the newly copied file so it can be included in the extensions map later srcFile.Close()
return nil, fmt.Errorf("failed to store application image '%s': %w", name, err)
}
srcFile.Close()
dstNameToExt[nameWithoutExt] = ext dstNameToExt[nameWithoutExt] = ext
} }
@@ -118,3 +126,11 @@ func mustDecodeHex(str string) []byte {
} }
return b return b
} }
func hashStream(r io.Reader) ([]byte, error) {
h := sha256.New()
if _, err := io.Copy(h, r); err != nil {
return nil, err
}
return h.Sum(nil), nil
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/job" "github.com/pocket-id/pocket-id/backend/internal/job"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
) )
@@ -21,7 +22,31 @@ func Bootstrap(ctx context.Context) error {
} }
slog.InfoContext(ctx, "Pocket ID is starting") slog.InfoContext(ctx, "Pocket ID is starting")
imageExtensions, err := initApplicationImages() // Initialize the file storage backend
var fileStorage storage.FileStorage
switch common.EnvConfig.FileBackend {
case storage.TypeFileSystem:
fileStorage, err = storage.NewFilesystemStorage(common.EnvConfig.UploadPath)
case storage.TypeS3:
s3Cfg := storage.S3Config{
Bucket: common.EnvConfig.S3Bucket,
Region: common.EnvConfig.S3Region,
Endpoint: common.EnvConfig.S3Endpoint,
AccessKeyID: common.EnvConfig.S3AccessKeyID,
SecretAccessKey: common.EnvConfig.S3SecretAccessKey,
ForcePathStyle: common.EnvConfig.S3ForcePathStyle,
Root: common.EnvConfig.UploadPath,
}
fileStorage, err = storage.NewS3Storage(ctx, s3Cfg)
default:
err = fmt.Errorf("unknown file storage backend: %s", common.EnvConfig.FileBackend)
}
if err != nil {
return fmt.Errorf("failed to initialize file storage: %w", err)
}
imageExtensions, err := initApplicationImages(ctx, fileStorage)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize application images: %w", err) return fmt.Errorf("failed to initialize application images: %w", err)
} }
@@ -33,7 +58,7 @@ func Bootstrap(ctx context.Context) error {
} }
// Create all services // Create all services
svc, err := initServices(ctx, db, httpClient, imageExtensions) svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize services: %w", err) return fmt.Errorf("failed to initialize services: %w", err)
} }

View File

@@ -17,7 +17,7 @@ import (
func init() { func init() {
registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){ registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){
func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) { func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
testService, err := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService) testService, err := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService, svc.fileStorage)
if err != nil { if err != nil {
slog.Error("Failed to initialize test service", slog.Any("error", err)) slog.Error("Failed to initialize test service", slog.Any("error", err))
os.Exit(1) os.Exit(1)

View File

@@ -23,7 +23,7 @@ func registerScheduledJobs(ctx context.Context, db *gorm.DB, svc *services, http
if err != nil { if err != nil {
return fmt.Errorf("failed to register DB cleanup jobs in scheduler: %w", err) return fmt.Errorf("failed to register DB cleanup jobs in scheduler: %w", err)
} }
err = scheduler.RegisterFileCleanupJobs(ctx, db) err = scheduler.RegisterFileCleanupJobs(ctx, db, svc.fileStorage)
if err != nil { if err != nil {
return fmt.Errorf("failed to register file cleanup jobs in scheduler: %w", err) return fmt.Errorf("failed to register file cleanup jobs in scheduler: %w", err)
} }

View File

@@ -8,6 +8,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/service" "github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/storage"
) )
type services struct { type services struct {
@@ -25,10 +26,11 @@ type services struct {
ldapService *service.LdapService ldapService *service.LdapService
apiKeyService *service.ApiKeyService apiKeyService *service.ApiKeyService
versionService *service.VersionService versionService *service.VersionService
fileStorage storage.FileStorage
} }
// Initializes all services // Initializes all services
func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string) (svc *services, err error) { func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string, fileStorage storage.FileStorage) (svc *services, err error) {
svc = &services{} svc = &services{}
svc.appConfigService, err = service.NewAppConfigService(ctx, db) svc.appConfigService, err = service.NewAppConfigService(ctx, db)
@@ -36,7 +38,8 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima
return nil, fmt.Errorf("failed to create app config service: %w", err) return nil, fmt.Errorf("failed to create app config service: %w", err)
} }
svc.appImagesService = service.NewAppImagesService(imageExtensions) svc.fileStorage = fileStorage
svc.appImagesService = service.NewAppImagesService(imageExtensions, fileStorage)
svc.emailService, err = service.NewEmailService(db, svc.appConfigService) svc.emailService, err = service.NewEmailService(db, svc.appConfigService)
if err != nil { if err != nil {
@@ -56,13 +59,13 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err) return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
} }
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, httpClient) svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, httpClient, fileStorage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create OIDC service: %w", err) return nil, fmt.Errorf("failed to create OIDC service: %w", err)
} }
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService) svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService) svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, fileStorage)
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService) svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService) svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)

View File

@@ -29,16 +29,24 @@ const (
DbProviderPostgres DbProvider = "postgres" DbProviderPostgres DbProvider = "postgres"
MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz" MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz"
defaultSqliteConnString string = "data/pocket-id.db" defaultSqliteConnString string = "data/pocket-id.db"
defaultFsUploadPath string = "data/uploads"
AppUrl string = "http://localhost:1411" AppUrl string = "http://localhost:1411"
) )
type EnvConfigSchema struct { type EnvConfigSchema struct {
AppEnv string `env:"APP_ENV" options:"toLower"` AppEnv string `env:"APP_ENV" options:"toLower"`
LogLevel string `env:"LOG_LEVEL" 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"` DbProvider DbProvider `env:"DB_PROVIDER" options:"toLower"`
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
FileBackend string `env:"FILE_BACKEND" options:"toLower"`
UploadPath string `env:"UPLOAD_PATH"` UploadPath string `env:"UPLOAD_PATH"`
S3Bucket string `env:"S3_BUCKET"`
S3Region string `env:"S3_REGION"`
S3Endpoint string `env:"S3_ENDPOINT"`
S3AccessKeyID string `env:"S3_ACCESS_KEY_ID"`
S3SecretAccessKey string `env:"S3_SECRET_ACCESS_KEY"`
S3ForcePathStyle bool `env:"S3_FORCE_PATH_STYLE"`
KeysPath string `env:"KEYS_PATH"` KeysPath string `env:"KEYS_PATH"`
KeysStorage string `env:"KEYS_STORAGE"` KeysStorage string `env:"KEYS_STORAGE"`
EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"` EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"`
@@ -75,27 +83,13 @@ func defaultConfig() EnvConfigSchema {
AppEnv: "production", AppEnv: "production",
LogLevel: "info", LogLevel: "info",
DbProvider: "sqlite", DbProvider: "sqlite",
DbConnectionString: "", FileBackend: "fs",
UploadPath: "data/uploads",
KeysPath: "data/keys", KeysPath: "data/keys",
KeysStorage: "", // "database" or "file"
EncryptionKey: nil,
AppURL: AppUrl, AppURL: AppUrl,
Port: "1411", Port: "1411",
Host: "0.0.0.0", Host: "0.0.0.0",
UnixSocket: "",
UnixSocketMode: "",
MaxMindLicenseKey: "",
GeoLiteDBPath: "data/GeoLite2-City.mmdb", GeoLiteDBPath: "data/GeoLite2-City.mmdb",
GeoLiteDBUrl: MaxMindGeoLiteCityUrl, GeoLiteDBUrl: MaxMindGeoLiteCityUrl,
LocalIPv6Ranges: "",
UiConfigDisabled: false,
MetricsEnabled: false,
TracingEnabled: false,
TrustProxy: false,
AnalyticsDisabled: false,
AllowDowngrade: false,
InternalAppURL: "",
} }
} }
@@ -181,6 +175,19 @@ func validateEnvConfig(config *EnvConfigSchema) error {
return fmt.Errorf("invalid value for KEYS_STORAGE: %s", config.KeysStorage) return fmt.Errorf("invalid value for KEYS_STORAGE: %s", config.KeysStorage)
} }
switch config.FileBackend {
case "s3":
if config.KeysStorage == "file" {
return errors.New("KEYS_STORAGE cannot be 'file' when FILE_BACKEND is 's3'")
}
case "", "fs":
if config.UploadPath == "" {
config.UploadPath = defaultFsUploadPath
}
default:
return errors.New("invalid FILE_BACKEND value. Must be 'fs' or 's3'")
}
// Validate LOCAL_IPV6_RANGES // Validate LOCAL_IPV6_RANGES
ranges := strings.Split(config.LocalIPv6Ranges, ",") ranges := strings.Split(config.LocalIPv6Ranges, ",")
for _, rangeStr := range ranges { for _, rangeStr := range ranges {
@@ -227,6 +234,10 @@ func prepareEnvConfig(config *EnvConfigSchema) error {
if err != nil { if err != nil {
return err return err
} }
case "trimTrailingSlash":
if field.Kind() == reflect.String {
field.SetString(strings.TrimRight(field.String(), "/"))
}
} }
} }
} }

View File

@@ -208,6 +208,44 @@ func TestParseEnvConfig(t *testing.T) {
assert.Equal(t, "8080", EnvConfig.Port) assert.Equal(t, "8080", EnvConfig.Port)
assert.Equal(t, "localhost", EnvConfig.Host) // lowercased assert.Equal(t, "localhost", EnvConfig.Host) // lowercased
}) })
t.Run("should normalize file backend and default upload path", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_PROVIDER", "sqlite")
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
t.Setenv("APP_URL", "http://localhost:3000")
t.Setenv("FILE_BACKEND", "FS")
t.Setenv("UPLOAD_PATH", "")
err := parseEnvConfig()
require.NoError(t, err)
assert.Equal(t, "fs", EnvConfig.FileBackend)
assert.Equal(t, defaultFsUploadPath, EnvConfig.UploadPath)
})
t.Run("should fail when FILE_BACKEND is s3 but keys are stored on filesystem", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_PROVIDER", "sqlite")
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
t.Setenv("APP_URL", "http://localhost:3000")
t.Setenv("FILE_BACKEND", "s3")
err := parseEnvConfig()
require.Error(t, err)
assert.ErrorContains(t, err, "KEYS_STORAGE cannot be 'file' when FILE_BACKEND is 's3'")
})
t.Run("should fail with invalid FILE_BACKEND value", func(t *testing.T) {
EnvConfig = defaultConfig()
t.Setenv("DB_PROVIDER", "sqlite")
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
t.Setenv("APP_URL", "http://localhost:3000")
t.Setenv("FILE_BACKEND", "invalid")
err := parseEnvConfig()
require.Error(t, err)
assert.ErrorContains(t, err, "invalid FILE_BACKEND value")
})
} }
func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) { func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {

View File

@@ -388,3 +388,13 @@ func (e *UserEmailNotSetError) Error() string {
func (e *UserEmailNotSetError) HttpStatusCode() int { func (e *UserEmailNotSetError) HttpStatusCode() int {
return http.StatusBadRequest return http.StatusBadRequest
} }
type ImageNotFoundError struct{}
func (e *ImageNotFoundError) Error() string {
return "Image not found"
}
func (e *ImageNotFoundError) HttpStatusCode() int {
return http.StatusNotFound
}

View File

@@ -25,10 +25,14 @@ func NewAppImagesController(
group.GET("/application-images/logo", controller.getLogoHandler) group.GET("/application-images/logo", controller.getLogoHandler)
group.GET("/application-images/background", controller.getBackgroundImageHandler) group.GET("/application-images/background", controller.getBackgroundImageHandler)
group.GET("/application-images/favicon", controller.getFaviconHandler) group.GET("/application-images/favicon", controller.getFaviconHandler)
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler) group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler) group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler) group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
group.DELETE("/application-images/default-profile-picture", authMiddleware.Add(), controller.deleteDefaultProfilePicture)
} }
type AppImagesController struct { type AppImagesController struct {
@@ -78,6 +82,18 @@ func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) {
c.getImage(ctx, "favicon") c.getImage(ctx, "favicon")
} }
// getDefaultProfilePicture godoc
// @Summary Get default profile picture image
// @Description Get the default profile picture image for the application
// @Tags Application Images
// @Produce image/png
// @Produce image/jpeg
// @Success 200 {file} binary "Default profile picture image"
// @Router /api/application-images/default-profile-picture [get]
func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) {
c.getImage(ctx, "default-profile-picture")
}
// updateLogoHandler godoc // updateLogoHandler godoc
// @Summary Update logo // @Summary Update logo
// @Description Update the application logo // @Description Update the application logo
@@ -100,7 +116,7 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
imageName = "logoDark" imageName = "logoDark"
} }
if err := c.appImagesService.UpdateImage(file, imageName); err != nil { if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, imageName); err != nil {
_ = ctx.Error(err) _ = ctx.Error(err)
return return
} }
@@ -123,7 +139,7 @@ func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) {
return return
} }
if err := c.appImagesService.UpdateImage(file, "background"); err != nil { if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "background"); err != nil {
_ = ctx.Error(err) _ = ctx.Error(err)
return return
} }
@@ -152,7 +168,7 @@ func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) {
return return
} }
if err := c.appImagesService.UpdateImage(file, "favicon"); err != nil { if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "favicon"); err != nil {
_ = ctx.Error(err) _ = ctx.Error(err)
return return
} }
@@ -161,13 +177,52 @@ func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) {
} }
func (c *AppImagesController) getImage(ctx *gin.Context, name string) { func (c *AppImagesController) getImage(ctx *gin.Context, name string) {
imagePath, mimeType, err := c.appImagesService.GetImage(name) reader, size, mimeType, err := c.appImagesService.GetImage(ctx.Request.Context(), name)
if err != nil {
_ = ctx.Error(err)
return
}
defer reader.Close()
ctx.Header("Content-Type", mimeType)
utils.SetCacheControlHeader(ctx, 15*time.Minute, 24*time.Hour)
ctx.DataFromReader(http.StatusOK, size, mimeType, reader, nil)
}
// updateDefaultProfilePicture godoc
// @Summary Update default profile picture image
// @Description Update the default profile picture image
// @Tags Application Images
// @Accept multipart/form-data
// @Param file formData file true "Profile picture image file"
// @Success 204 "No Content"
// @Router /api/application-images/default-profile-picture [put]
func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) {
file, err := ctx.FormFile("file")
if err != nil { if err != nil {
_ = ctx.Error(err) _ = ctx.Error(err)
return return
} }
ctx.Header("Content-Type", mimeType) if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "default-profile-picture"); err != nil {
utils.SetCacheControlHeader(ctx, 15*time.Minute, 24*time.Hour) _ = ctx.Error(err)
ctx.File(imagePath) return
}
ctx.Status(http.StatusNoContent)
}
// deleteDefaultProfilePicture godoc
// @Summary Delete default profile picture image
// @Description Delete the default profile picture image
// @Tags Application Images
// @Success 204 "No Content"
// @Router /api/application-images/default-profile-picture [delete]
func (c *AppImagesController) deleteDefaultProfilePicture(ctx *gin.Context) {
if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "default-profile-picture"); err != nil {
_ = ctx.Error(err)
return
}
ctx.Status(http.StatusNoContent)
} }

View File

@@ -548,16 +548,17 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
func (oc *OidcController) getClientLogoHandler(c *gin.Context) { func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true")) lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo) reader, size, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo)
if err != nil { if err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
defer reader.Close()
utils.SetCacheControlHeader(c, 15*time.Minute, 12*time.Hour) utils.SetCacheControlHeader(c, 15*time.Minute, 12*time.Hour)
c.Header("Content-Type", mimeType) c.Header("Content-Type", mimeType)
c.File(imagePath) c.DataFromReader(http.StatusOK, size, mimeType, reader, nil)
} }
// updateClientLogoHandler godoc // updateClientLogoHandler godoc

View File

@@ -286,7 +286,7 @@ func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
} }
defer file.Close() defer file.Close()
if err := uc.userService.UpdateProfilePicture(userID, file); err != nil { if err := uc.userService.UpdateProfilePicture(c.Request.Context(), userID, file); err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
@@ -317,7 +317,7 @@ func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context)
} }
defer file.Close() defer file.Close()
if err := uc.userService.UpdateProfilePicture(userID, file); err != nil { if err := uc.userService.UpdateProfilePicture(c.Request.Context(), userID, file); err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
@@ -687,7 +687,7 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) { func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
userID := c.Param("id") userID := c.Param("id")
if err := uc.userService.ResetProfilePicture(userID); err != nil { if err := uc.userService.ResetProfilePicture(c.Request.Context(), userID); err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }
@@ -705,7 +705,7 @@ func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) { func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) {
userID := c.GetString("userID") userID := c.GetString("userID")
if err := uc.userService.ResetProfilePicture(userID); err != nil { if err := uc.userService.ResetProfilePicture(c.Request.Context(), userID); err != nil {
_ = c.Error(err) _ = c.Error(err)
return return
} }

View File

@@ -2,29 +2,36 @@ package job
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "path"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/storage"
) )
func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) error { func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB, fileStorage storage.FileStorage) error {
jobs := &FileCleanupJobs{db: db} jobs := &FileCleanupJobs{db: db, fileStorage: fileStorage}
// Run every 24 hours err := s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false)
return s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false)
// Only necessary for file system storage
if fileStorage.Type() == storage.TypeFileSystem {
err = errors.Join(err, s.registerJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, true))
}
return err
} }
type FileCleanupJobs struct { type FileCleanupJobs struct {
db *gorm.DB db *gorm.DB
fileStorage storage.FileStorage
} }
// ClearUnusedDefaultProfilePictures deletes default profile pictures that don't match any user's initials // ClearUnusedDefaultProfilePictures deletes default profile pictures that don't match any user's initials
@@ -44,29 +51,24 @@ func (j *FileCleanupJobs) clearUnusedDefaultProfilePictures(ctx context.Context)
initialsInUse[user.Initials()] = struct{}{} initialsInUse[user.Initials()] = struct{}{}
} }
defaultPicturesDir := common.EnvConfig.UploadPath + "/profile-pictures/defaults" defaultPicturesDir := path.Join("profile-pictures", "defaults")
if _, err := os.Stat(defaultPicturesDir); os.IsNotExist(err) { files, err := j.fileStorage.List(ctx, defaultPicturesDir)
return nil
}
files, err := os.ReadDir(defaultPicturesDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to read default profile pictures directory: %w", err) return fmt.Errorf("failed to list default profile pictures: %w", err)
} }
filesDeleted := 0 filesDeleted := 0
for _, file := range files { for _, file := range files {
if file.IsDir() { _, filename := path.Split(file.Path)
continue // Skip directories if filename == "" {
continue
} }
filename := file.Name()
initials := strings.TrimSuffix(filename, ".png") initials := strings.TrimSuffix(filename, ".png")
// If these initials aren't used by any user, delete the file // If these initials aren't used by any user, delete the file
if _, ok := initialsInUse[initials]; !ok { if _, ok := initialsInUse[initials]; !ok {
filePath := filepath.Join(defaultPicturesDir, filename) filePath := path.Join(defaultPicturesDir, filename)
if err := os.Remove(filePath); err != nil { if err := j.fileStorage.Delete(ctx, filePath); err != nil {
slog.ErrorContext(ctx, "Failed to delete unused default profile picture", slog.String("path", filePath), slog.Any("error", err)) slog.ErrorContext(ctx, "Failed to delete unused default profile picture", slog.String("path", filePath), slog.Any("error", err))
} else { } else {
filesDeleted++ filesDeleted++
@@ -77,3 +79,34 @@ func (j *FileCleanupJobs) clearUnusedDefaultProfilePictures(ctx context.Context)
slog.Info("Done deleting unused default profile pictures", slog.Int("count", filesDeleted)) slog.Info("Done deleting unused default profile pictures", slog.Int("count", filesDeleted))
return nil return nil
} }
// clearOrphanedTempFiles deletes temporary files that are produced by failed atomic writes
func (j *FileCleanupJobs) clearOrphanedTempFiles(ctx context.Context) error {
const minAge = 10 * time.Minute
var deleted int
err := j.fileStorage.Walk(ctx, "/", func(p storage.ObjectInfo) error {
// Only temp files
if !strings.HasSuffix(p.Path, "-tmp") {
return nil
}
if time.Since(p.ModTime) < minAge {
return nil
}
if err := j.fileStorage.Delete(ctx, p.Path); err != nil {
slog.ErrorContext(ctx, "Failed to delete temp file", slog.String("path", p.Path), slog.Any("error", err))
return nil
}
deleted++
return nil
})
if err != nil {
return fmt.Errorf("failed to scan storage: %w", err)
}
slog.Info("Done cleaning orphaned temp files", slog.Int("count", deleted))
return nil
}

View File

@@ -54,8 +54,8 @@ type OidcClient struct {
ImageType *string ImageType *string
DarkImageType *string DarkImageType *string
IsPublic bool IsPublic bool
PkceEnabled bool `filterable:"true"` PkceEnabled bool `sortable:"true" filterable:"true"`
RequiresReauthentication bool `filterable:"true"` RequiresReauthentication bool `sortable:"true" filterable:"true"`
Credentials OidcClientCredentials Credentials OidcClientCredentials
LaunchURL *string LaunchURL *string

View File

@@ -1,42 +1,52 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"io"
"mime/multipart" "mime/multipart"
"os" "path"
"path/filepath"
"strings" "strings"
"sync" "sync"
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
) )
type AppImagesService struct { type AppImagesService struct {
mu sync.RWMutex mu sync.RWMutex
extensions map[string]string extensions map[string]string
storage storage.FileStorage
} }
func NewAppImagesService(extensions map[string]string) *AppImagesService { func NewAppImagesService(extensions map[string]string, storage storage.FileStorage) *AppImagesService {
return &AppImagesService{extensions: extensions} return &AppImagesService{extensions: extensions, storage: storage}
} }
func (s *AppImagesService) GetImage(name string) (string, string, error) { func (s *AppImagesService) GetImage(ctx context.Context, name string) (io.ReadCloser, int64, string, error) {
ext, err := s.getExtension(name) ext, err := s.getExtension(name)
if err != nil { if err != nil {
return "", "", err return nil, 0, "", err
} }
mimeType := utils.GetImageMimeType(ext) mimeType := utils.GetImageMimeType(ext)
if mimeType == "" { if mimeType == "" {
return "", "", fmt.Errorf("unsupported image type '%s'", ext) return nil, 0, "", fmt.Errorf("unsupported image type '%s'", ext)
} }
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", name, ext)) imagePath := path.Join("application-images", name+"."+ext)
return imagePath, mimeType, nil reader, size, err := s.storage.Open(ctx, imagePath)
if err != nil {
if storage.IsNotExist(err) {
return nil, 0, "", &common.ImageNotFoundError{}
}
return nil, 0, "", err
}
return reader, size, mimeType, nil
} }
func (s *AppImagesService) UpdateImage(file *multipart.FileHeader, imageName string) error { func (s *AppImagesService) UpdateImage(ctx context.Context, file *multipart.FileHeader, imageName string) error {
fileType := strings.ToLower(utils.GetFileExtension(file.Filename)) fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
mimeType := utils.GetImageMimeType(fileType) mimeType := utils.GetImageMimeType(fileType)
if mimeType == "" { if mimeType == "" {
@@ -48,18 +58,23 @@ func (s *AppImagesService) UpdateImage(file *multipart.FileHeader, imageName str
currentExt, ok := s.extensions[imageName] currentExt, ok := s.extensions[imageName]
if !ok { if !ok {
return fmt.Errorf("unknown application image '%s'", imageName) s.extensions[imageName] = fileType
} }
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", imageName, fileType)) imagePath := path.Join("application-images", imageName+"."+fileType)
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
if err := utils.SaveFile(file, imagePath); err != nil { if err := s.storage.Save(ctx, imagePath, fileReader); err != nil {
return err return err
} }
if currentExt != "" && currentExt != fileType { if currentExt != "" && currentExt != fileType {
oldImagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", imageName, currentExt)) oldImagePath := path.Join("application-images", imageName+"."+currentExt)
if err := os.Remove(oldImagePath); err != nil && !os.IsNotExist(err) { if err := s.storage.Delete(ctx, oldImagePath); err != nil {
return err return err
} }
} }
@@ -69,13 +84,39 @@ func (s *AppImagesService) UpdateImage(file *multipart.FileHeader, imageName str
return nil return nil
} }
func (s *AppImagesService) DeleteImage(ctx context.Context, imageName string) error {
s.mu.Lock()
defer s.mu.Unlock()
ext, ok := s.extensions[imageName]
if !ok || ext == "" {
return &common.ImageNotFoundError{}
}
imagePath := path.Join("application-images", imageName+"."+ext)
if err := s.storage.Delete(ctx, imagePath); err != nil {
return err
}
delete(s.extensions, imageName)
return nil
}
func (s *AppImagesService) IsDefaultProfilePictureSet() bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.extensions["default-profile-picture"]
return ok
}
func (s *AppImagesService) getExtension(name string) (string, error) { func (s *AppImagesService) getExtension(name string) (string, error) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
ext, ok := s.extensions[name] ext, ok := s.extensions[name]
if !ok || ext == "" { if !ok || ext == "" {
return "", fmt.Errorf("unknown application image '%s'", name) return "", &common.ImageNotFoundError{}
} }
return strings.ToLower(ext), nil return strings.ToLower(ext), nil

View File

@@ -2,66 +2,92 @@ package service
import ( import (
"bytes" "bytes"
"context"
"io"
"io/fs" "io/fs"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "path"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/storage"
) )
func TestAppImagesService_GetImage(t *testing.T) { func TestAppImagesService_GetImage(t *testing.T) {
tempDir := t.TempDir() store, err := storage.NewFilesystemStorage(t.TempDir())
originalUploadPath := common.EnvConfig.UploadPath
common.EnvConfig.UploadPath = tempDir
t.Cleanup(func() {
common.EnvConfig.UploadPath = originalUploadPath
})
imagesDir := filepath.Join(tempDir, "application-images")
require.NoError(t, os.MkdirAll(imagesDir, 0o755))
filePath := filepath.Join(imagesDir, "background.webp")
require.NoError(t, os.WriteFile(filePath, []byte("data"), fs.FileMode(0o644)))
service := NewAppImagesService(map[string]string{"background": "webp"})
path, mimeType, err := service.GetImage("background")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, filePath, path)
require.NoError(t, store.Save(context.Background(), path.Join("application-images", "background.webp"), bytes.NewReader([]byte("data"))))
service := NewAppImagesService(map[string]string{"background": "webp"}, store)
reader, size, mimeType, err := service.GetImage(context.Background(), "background")
require.NoError(t, err)
defer reader.Close()
payload, err := io.ReadAll(reader)
require.NoError(t, err)
require.Equal(t, []byte("data"), payload)
require.Equal(t, int64(len(payload)), size)
require.Equal(t, "image/webp", mimeType) require.Equal(t, "image/webp", mimeType)
} }
func TestAppImagesService_UpdateImage(t *testing.T) { func TestAppImagesService_UpdateImage(t *testing.T) {
tempDir := t.TempDir() store, err := storage.NewFilesystemStorage(t.TempDir())
originalUploadPath := common.EnvConfig.UploadPath require.NoError(t, err)
common.EnvConfig.UploadPath = tempDir
t.Cleanup(func() {
common.EnvConfig.UploadPath = originalUploadPath
})
imagesDir := filepath.Join(tempDir, "application-images") require.NoError(t, store.Save(context.Background(), path.Join("application-images", "logoLight.svg"), bytes.NewReader([]byte("old"))))
require.NoError(t, os.MkdirAll(imagesDir, 0o755))
oldPath := filepath.Join(imagesDir, "logoLight.svg") service := NewAppImagesService(map[string]string{"logoLight": "svg"}, store)
require.NoError(t, os.WriteFile(oldPath, []byte("old"), fs.FileMode(0o644)))
service := NewAppImagesService(map[string]string{"logoLight": "svg"})
fileHeader := newFileHeader(t, "logoLight.png", []byte("new")) fileHeader := newFileHeader(t, "logoLight.png", []byte("new"))
require.NoError(t, service.UpdateImage(fileHeader, "logoLight")) require.NoError(t, service.UpdateImage(context.Background(), fileHeader, "logoLight"))
_, err := os.Stat(filepath.Join(imagesDir, "logoLight.png")) reader, _, err := store.Open(context.Background(), path.Join("application-images", "logoLight.png"))
require.NoError(t, err)
_ = reader.Close()
_, _, err = store.Open(context.Background(), path.Join("application-images", "logoLight.svg"))
require.ErrorIs(t, err, fs.ErrNotExist)
}
func TestAppImagesService_ErrorsAndFlags(t *testing.T) {
store, err := storage.NewFilesystemStorage(t.TempDir())
require.NoError(t, err) require.NoError(t, err)
_, err = os.Stat(oldPath) service := NewAppImagesService(map[string]string{}, store)
require.ErrorIs(t, err, os.ErrNotExist)
t.Run("get missing image returns not found", func(t *testing.T) {
_, _, _, err := service.GetImage(context.Background(), "missing")
require.Error(t, err)
var imageErr *common.ImageNotFoundError
assert.ErrorAs(t, err, &imageErr)
})
t.Run("reject unsupported file types", func(t *testing.T) {
err := service.UpdateImage(context.Background(), newFileHeader(t, "logo.txt", []byte("nope")), "logo")
require.Error(t, err)
var fileTypeErr *common.FileTypeNotSupportedError
assert.ErrorAs(t, err, &fileTypeErr)
})
t.Run("delete and extension tracking", func(t *testing.T) {
require.NoError(t, store.Save(context.Background(), path.Join("application-images", "default-profile-picture.png"), bytes.NewReader([]byte("img"))))
service.extensions["default-profile-picture"] = "png"
require.NoError(t, service.DeleteImage(context.Background(), "default-profile-picture"))
assert.False(t, service.IsDefaultProfilePictureSet())
err := service.DeleteImage(context.Background(), "default-profile-picture")
require.Error(t, err)
var imageErr *common.ImageNotFoundError
assert.ErrorAs(t, err, &imageErr)
})
} }
func newFileHeader(t *testing.T, filename string, content []byte) *multipart.FileHeader { func newFileHeader(t *testing.T, filename string, content []byte) *multipart.FileHeader {

View File

@@ -11,8 +11,7 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "path"
"path/filepath"
"time" "time"
"github.com/fxamacker/cbor/v2" "github.com/fxamacker/cbor/v2"
@@ -25,6 +24,7 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk" jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
"github.com/pocket-id/pocket-id/backend/resources" "github.com/pocket-id/pocket-id/backend/resources"
@@ -35,15 +35,17 @@ type TestService struct {
jwtService *JwtService jwtService *JwtService
appConfigService *AppConfigService appConfigService *AppConfigService
ldapService *LdapService ldapService *LdapService
fileStorage storage.FileStorage
externalIdPKey jwk.Key externalIdPKey jwk.Key
} }
func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService) (*TestService, error) { func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService, fileStorage storage.FileStorage) (*TestService, error) {
s := &TestService{ s := &TestService{
db: db, db: db,
appConfigService: appConfigService, appConfigService: appConfigService,
jwtService: jwtService, jwtService: jwtService,
ldapService: ldapService, ldapService: ldapService,
fileStorage: fileStorage,
} }
err := s.initExternalIdP() err := s.initExternalIdP()
if err != nil { if err != nil {
@@ -424,8 +426,8 @@ func (s *TestService) ResetDatabase() error {
} }
func (s *TestService) ResetApplicationImages(ctx context.Context) error { func (s *TestService) ResetApplicationImages(ctx context.Context) error {
if err := os.RemoveAll(common.EnvConfig.UploadPath); err != nil { if err := s.fileStorage.DeleteAll(ctx, "/"); err != nil {
slog.ErrorContext(ctx, "Error removing directory", slog.Any("error", err)) slog.ErrorContext(ctx, "Error removing uploads", slog.Any("error", err))
return err return err
} }
@@ -435,13 +437,19 @@ func (s *TestService) ResetApplicationImages(ctx context.Context) error {
} }
for _, file := range files { for _, file := range files {
srcFilePath := filepath.Join("images", file.Name()) if file.IsDir() {
destFilePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", file.Name()) continue
}
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath) srcFilePath := path.Join("images", file.Name())
srcFile, err := resources.FS.Open(srcFilePath)
if err != nil { if err != nil {
return err return err
} }
if err := s.fileStorage.Save(ctx, path.Join("application-images", file.Name()), srcFile); err != nil {
srcFile.Close()
return err
}
srcFile.Close()
} }
return nil return nil

View File

@@ -282,16 +282,18 @@ func prepareBody[V any](srv *EmailService, template email.Template[V], data *ema
var htmlHeader = textproto.MIMEHeader{} var htmlHeader = textproto.MIMEHeader{}
htmlHeader.Add("Content-Type", "text/html; charset=UTF-8") htmlHeader.Add("Content-Type", "text/html; charset=UTF-8")
htmlHeader.Add("Content-Transfer-Encoding", "8bit") htmlHeader.Add("Content-Transfer-Encoding", "quoted-printable")
htmlPart, err := mpart.CreatePart(htmlHeader) htmlPart, err := mpart.CreatePart(htmlHeader)
if err != nil { if err != nil {
return "", "", fmt.Errorf("create html part: %w", err) return "", "", fmt.Errorf("create html part: %w", err)
} }
err = email.GetTemplate(srv.htmlTemplates, template).ExecuteTemplate(htmlPart, "root", data) htmlQp := quotedprintable.NewWriter(htmlPart)
err = email.GetTemplate(srv.htmlTemplates, template).ExecuteTemplate(htmlQp, "root", data)
if err != nil { if err != nil {
return "", "", fmt.Errorf("execute html template: %w", err) return "", "", fmt.Errorf("execute html template: %w", err)
} }
htmlQp.Close()
err = mpart.Close() err = mpart.Close()
if err != nil { if err != nil {

View File

@@ -470,7 +470,7 @@ func (s *LdapService) saveProfilePicture(parentCtx context.Context, userId strin
} }
// Update the profile picture // Update the profile picture
err = s.userService.UpdateProfilePicture(userId, reader) err = s.userService.UpdateProfilePicture(parentCtx, userId, reader)
if err != nil { if err != nil {
return fmt.Errorf("failed to update profile picture: %w", err) return fmt.Errorf("failed to update profile picture: %w", err)
} }

View File

@@ -15,8 +15,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "path"
"path/filepath"
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
@@ -35,6 +34,7 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/dto" "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/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
) )
@@ -61,6 +61,7 @@ type OidcService struct {
httpClient *http.Client httpClient *http.Client
jwkCache *jwk.Cache jwkCache *jwk.Cache
fileStorage storage.FileStorage
} }
func NewOidcService( func NewOidcService(
@@ -72,6 +73,7 @@ func NewOidcService(
customClaimService *CustomClaimService, customClaimService *CustomClaimService,
webAuthnService *WebAuthnService, webAuthnService *WebAuthnService,
httpClient *http.Client, httpClient *http.Client,
fileStorage storage.FileStorage,
) (s *OidcService, err error) { ) (s *OidcService, err error) {
s = &OidcService{ s = &OidcService{
db: db, db: db,
@@ -81,6 +83,7 @@ func NewOidcService(
customClaimService: customClaimService, customClaimService: customClaimService,
webAuthnService: webAuthnService, webAuthnService: webAuthnService,
httpClient: httpClient, httpClient: httpClient,
fileStorage: fileStorage,
} }
// Note: we don't pass the HTTP Client with OTel instrumented to this because requests are always made in background and not tied to a specific trace // Note: we don't pass the HTTP Client with OTel instrumented to this because requests are always made in background and not tied to a specific trace
@@ -884,34 +887,41 @@ func (s *OidcService) CreateClientSecret(ctx context.Context, clientID string) (
return clientSecret, nil return clientSecret, nil
} }
func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light bool) (string, string, error) { func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light bool) (io.ReadCloser, int64, string, error) {
var client model.OidcClient var client model.OidcClient
err := s.db. err := s.db.
WithContext(ctx). WithContext(ctx).
First(&client, "id = ?", clientID). First(&client, "id = ?", clientID).
Error Error
if err != nil { if err != nil {
return "", "", err return nil, 0, "", err
} }
var imagePath, mimeType string var suffix string
var ext string
switch { switch {
case !light && client.DarkImageType != nil: case !light && client.DarkImageType != nil:
// Dark logo if requested and exists // Dark logo if requested and exists
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + *client.DarkImageType suffix = "-dark"
mimeType = utils.GetImageMimeType(*client.DarkImageType) ext = *client.DarkImageType
case client.ImageType != nil: case client.ImageType != nil:
// Light logo if requested or no dark logo is available // Light logo if requested or no dark logo is available
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType ext = *client.ImageType
mimeType = utils.GetImageMimeType(*client.ImageType)
default: default:
return "", "", errors.New("image not found") return nil, 0, "", errors.New("image not found")
} }
return imagePath, mimeType, nil mimeType := utils.GetImageMimeType(ext)
if mimeType == "" {
return nil, 0, "", fmt.Errorf("unsupported image type '%s'", ext)
}
key := path.Join("oidc-client-images", client.ID+suffix+"."+ext)
reader, size, err := s.fileStorage.Open(ctx, key)
if err != nil {
return nil, 0, "", err
}
return reader, size, mimeType, nil
} }
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader, light bool) error { func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader, light bool) error {
@@ -925,11 +935,15 @@ func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, fil
darkSuffix = "-dark" darkSuffix = "-dark"
} }
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + darkSuffix + "." + fileType imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+fileType)
err := utils.SaveFile(file, imagePath) reader, err := file.Open()
if err != nil { if err != nil {
return err return err
} }
defer reader.Close()
if err := s.fileStorage.Save(ctx, imagePath, reader); err != nil {
return err
}
tx := s.db.Begin() tx := s.db.Begin()
@@ -972,8 +986,8 @@ func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) err
return err return err
} }
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + oldImageType imagePath := path.Join("oidc-client-images", client.ID+"."+oldImageType)
if err := os.Remove(imagePath); err != nil { if err := s.fileStorage.Delete(ctx, imagePath); err != nil {
return err return err
} }
@@ -1015,8 +1029,8 @@ func (s *OidcService) DeleteClientDarkLogo(ctx context.Context, clientID string)
return err return err
} }
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + oldImageType imagePath := path.Join("oidc-client-images", client.ID+"-dark."+oldImageType)
if err := os.Remove(imagePath); err != nil { if err := s.fileStorage.Delete(ctx, imagePath); err != nil {
return err return err
} }
@@ -2017,20 +2031,13 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *
return &common.FileTypeNotSupportedError{} return &common.FileTypeNotSupportedError{}
} }
folderPath := filepath.Join(common.EnvConfig.UploadPath, "oidc-client-images")
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
return err
}
var darkSuffix string var darkSuffix string
if !light { if !light {
darkSuffix = "-dark" darkSuffix = "-dark"
} }
imagePath := filepath.Join(folderPath, clientID+darkSuffix+"."+ext) imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+ext)
err = utils.SaveFileStream(io.LimitReader(resp.Body, maxLogoSize+1), imagePath) if err := s.fileStorage.Save(ctx, imagePath, io.LimitReader(resp.Body, maxLogoSize+1)); err != nil {
if err != nil {
return err return err
} }
@@ -2042,8 +2049,6 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *
} }
func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, clientID, ext string, light bool) 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 var darkSuffix string
if !light { if !light {
darkSuffix = "-dark" darkSuffix = "-dark"
@@ -2053,9 +2058,15 @@ func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, cli
if err := tx.WithContext(ctx).First(&client, "id = ?", clientID).Error; err != nil { if err := tx.WithContext(ctx).First(&client, "id = ?", clientID).Error; err != nil {
return err return err
} }
if client.ImageType != nil && *client.ImageType != ext { var currentType *string
old := fmt.Sprintf("%s/%s%s.%s", uploadsDir, client.ID, darkSuffix, *client.ImageType) if light {
_ = os.Remove(old) currentType = client.ImageType
} else {
currentType = client.DarkImageType
}
if currentType != nil && *currentType != ext {
old := path.Join("oidc-client-images", client.ID+darkSuffix+"."+*currentType)
_ = s.fileStorage.Delete(ctx, old)
} }
var column string var column string

View File

@@ -7,9 +7,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log/slog" "log/slog"
"net/url" "net/url"
"os" "path"
"strings" "strings"
"time" "time"
@@ -21,6 +22,7 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/dto" "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/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/internal/utils/email" "github.com/pocket-id/pocket-id/backend/internal/utils/email"
profilepicture "github.com/pocket-id/pocket-id/backend/internal/utils/image" profilepicture "github.com/pocket-id/pocket-id/backend/internal/utils/image"
@@ -33,9 +35,11 @@ type UserService struct {
emailService *EmailService emailService *EmailService
appConfigService *AppConfigService appConfigService *AppConfigService
customClaimService *CustomClaimService customClaimService *CustomClaimService
appImagesService *AppImagesService
fileStorage storage.FileStorage
} }
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService) *UserService { func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService, fileStorage storage.FileStorage) *UserService {
return &UserService{ return &UserService{
db: db, db: db,
jwtService: jwtService, jwtService: jwtService,
@@ -43,6 +47,8 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
emailService: emailService, emailService: emailService,
appConfigService: appConfigService, appConfigService: appConfigService,
customClaimService: customClaimService, customClaimService: customClaimService,
appImagesService: appImagesService,
fileStorage: fileStorage,
} }
} }
@@ -87,60 +93,56 @@ func (s *UserService) GetProfilePicture(ctx context.Context, userID string) (io.
return nil, 0, &common.InvalidUUIDError{} return nil, 0, &common.InvalidUUIDError{}
} }
// First check for a custom uploaded profile picture (userID.png)
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png"
file, err := os.Open(profilePicturePath)
if err == nil {
// Get the file size
fileInfo, err := file.Stat()
if err != nil {
file.Close()
return nil, 0, err
}
return file, fileInfo.Size(), nil
}
// If no custom picture exists, get the user's data for creating initials
user, err := s.GetUser(ctx, userID) user, err := s.GetUser(ctx, userID)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
// Check if we have a cached default picture for these initials profilePicturePath := path.Join("profile-pictures", userID+".png")
defaultProfilePicturesDir := common.EnvConfig.UploadPath + "/profile-pictures/defaults/"
defaultPicturePath := defaultProfilePicturesDir + user.Initials() + ".png" // Try custom profile picture
file, err = os.Open(defaultPicturePath) if file, size, err := s.fileStorage.Open(ctx, profilePicturePath); err == nil {
if err == nil { return file, size, nil
fileInfo, err := file.Stat() } else if err != nil && !errors.Is(err, fs.ErrNotExist) {
if err != nil { return nil, 0, err
file.Close() }
// Try default global profile picture
if s.appImagesService.IsDefaultProfilePictureSet() {
reader, size, _, err := s.appImagesService.GetImage(ctx, "default-profile-picture")
if err == nil {
return reader, size, nil
}
if !errors.Is(err, &common.ImageNotFoundError{}) {
return nil, 0, err return nil, 0, err
} }
return file, fileInfo.Size(), nil
} }
// If no cached default picture exists, create one and save it for future use // Try cached default for initials
defaultPicturePath := path.Join("profile-pictures", "defaults", user.Initials()+".png")
if file, size, err := s.fileStorage.Open(ctx, defaultPicturePath); err == nil {
return file, size, nil
} else if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, 0, err
}
// Create and return generated default with initials
defaultPicture, err := profilepicture.CreateDefaultProfilePicture(user.Initials()) defaultPicture, err := profilepicture.CreateDefaultProfilePicture(user.Initials())
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
// Save the default picture for future use (in a goroutine to avoid blocking) // Save the default picture for future use (in a goroutine to avoid blocking)
//nolint:contextcheck
defaultPictureBytes := defaultPicture.Bytes() defaultPictureBytes := defaultPicture.Bytes()
//nolint:contextcheck
go func() { go func() {
// Ensure the directory exists if err := s.fileStorage.Save(context.Background(), defaultPicturePath, bytes.NewReader(defaultPictureBytes)); err != nil {
errInternal := os.MkdirAll(defaultProfilePicturesDir, os.ModePerm) slog.Error("Failed to cache default profile picture", slog.String("initials", user.Initials()), slog.Any("error", err))
if errInternal != nil {
slog.Error("Failed to create directory for default profile picture", slog.Any("error", errInternal))
return
}
errInternal = utils.SaveFileStream(bytes.NewReader(defaultPictureBytes), defaultPicturePath)
if errInternal != nil {
slog.Error("Failed to cache default profile picture for initials", slog.String("initials", user.Initials()), slog.Any("error", errInternal))
} }
}() }()
return io.NopCloser(bytes.NewReader(defaultPictureBytes)), int64(defaultPicture.Len()), nil return io.NopCloser(bytes.NewReader(defaultPictureBytes)), int64(len(defaultPictureBytes)), nil
} }
func (s *UserService) GetUserGroups(ctx context.Context, userID string) ([]model.UserGroup, error) { func (s *UserService) GetUserGroups(ctx context.Context, userID string) ([]model.UserGroup, error) {
@@ -157,7 +159,7 @@ func (s *UserService) GetUserGroups(ctx context.Context, userID string) ([]model
return user.UserGroups, nil return user.UserGroups, nil
} }
func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error { func (s *UserService) UpdateProfilePicture(ctx context.Context, userID string, file io.Reader) error {
// Validate the user ID to prevent directory traversal // Validate the user ID to prevent directory traversal
err := uuid.Validate(userID) err := uuid.Validate(userID)
if err != nil { if err != nil {
@@ -170,15 +172,8 @@ func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error
return err return err
} }
// Ensure the directory exists profilePicturePath := path.Join("profile-pictures", userID+".png")
profilePictureDir := common.EnvConfig.UploadPath + "/profile-pictures" err = s.fileStorage.Save(ctx, profilePicturePath, profilePicture)
err = os.MkdirAll(profilePictureDir, os.ModePerm)
if err != nil {
return err
}
// Create the profile picture file
err = utils.SaveFileStream(profilePicture, profilePictureDir+"/"+userID+".png")
if err != nil { if err != nil {
return err return err
} }
@@ -209,10 +204,8 @@ func (s *UserService) deleteUserInternal(ctx context.Context, userID string, all
return &common.LdapUserUpdateError{} return &common.LdapUserUpdateError{}
} }
// Delete the profile picture profilePicturePath := path.Join("profile-pictures", userID+".png")
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png" if err := s.fileStorage.Delete(ctx, profilePicturePath); err != nil {
err = os.Remove(profilePicturePath)
if err != nil && !os.IsNotExist(err) {
return err return err
} }
@@ -256,6 +249,7 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
Username: input.Username, Username: input.Username,
IsAdmin: input.IsAdmin, IsAdmin: input.IsAdmin,
Locale: input.Locale, Locale: input.Locale,
Disabled: input.Disabled,
} }
if input.LdapID != "" { if input.LdapID != "" {
user.LdapID = &input.LdapID user.LdapID = &input.LdapID
@@ -672,26 +666,16 @@ func (s *UserService) checkDuplicatedFields(ctx context.Context, user model.User
} }
// ResetProfilePicture deletes a user's custom profile picture // ResetProfilePicture deletes a user's custom profile picture
func (s *UserService) ResetProfilePicture(userID string) error { func (s *UserService) ResetProfilePicture(ctx context.Context, userID string) error {
// Validate the user ID to prevent directory traversal // Validate the user ID to prevent directory traversal
if err := uuid.Validate(userID); err != nil { if err := uuid.Validate(userID); err != nil {
return &common.InvalidUUIDError{} return &common.InvalidUUIDError{}
} }
// Build path to profile picture profilePicturePath := path.Join("profile-pictures", userID+".png")
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png" if err := s.fileStorage.Delete(ctx, profilePicturePath); err != nil {
// Check if file exists and delete it
if _, err := os.Stat(profilePicturePath); err == nil {
if err := os.Remove(profilePicturePath); err != nil {
return fmt.Errorf("failed to delete profile picture: %w", err) return fmt.Errorf("failed to delete profile picture: %w", err)
} }
} else if !os.IsNotExist(err) {
// If any error other than "file not exists"
return fmt.Errorf("failed to check if profile picture exists: %w", err)
}
// It's okay if the file doesn't exist - just means there's no custom picture to delete
return nil return nil
} }

View File

@@ -81,6 +81,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
&user, &user,
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired), webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
webauthn.WithExclusions(user.WebAuthnCredentialDescriptors()), 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 { if err != nil {
return nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err) return nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)

View File

@@ -0,0 +1,193 @@
package storage
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
)
type filesystemStorage struct {
root *os.Root
absoluteRootPath string
}
func NewFilesystemStorage(rootPath string) (FileStorage, error) {
if err := os.MkdirAll(rootPath, 0700); err != nil {
return nil, fmt.Errorf("failed to create root directory '%s': %w", rootPath, err)
}
root, err := os.OpenRoot(rootPath)
if err != nil {
return nil, fmt.Errorf("failed to open root directory '%s': %w", rootPath, err)
}
absoluteRootPath, err := filepath.Abs(rootPath)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path of root directory '%s': %w", rootPath, err)
}
return &filesystemStorage{root: root, absoluteRootPath: absoluteRootPath}, err
}
func (s *filesystemStorage) Type() string {
return TypeFileSystem
}
func (s *filesystemStorage) Save(_ context.Context, path string, data io.Reader) error {
path = filepath.FromSlash(path)
if err := s.root.MkdirAll(filepath.Dir(path), 0700); err != nil {
return fmt.Errorf("failed to create directories for path '%s': %w", path, err)
}
// Our strategy is to save to a separate file and then rename it to override the original file
tmpName := path + "." + uuid.NewString() + "-tmp"
// Write to the temporary file
tmpFile, err := s.root.Create(tmpName)
if err != nil {
return fmt.Errorf("failed to open file '%s' for writing: %w", tmpName, err)
}
_, err = io.Copy(tmpFile, data)
if err != nil {
tmpFile.Close()
_ = s.root.Remove(tmpName)
return fmt.Errorf("failed to write temporary file: %w", err)
}
if err = tmpFile.Close(); err != nil {
_ = s.root.Remove(tmpName)
return fmt.Errorf("failed to close temporary file: %w", err)
}
// Rename to the final file, which overrides existing files
// This is an atomic operation
if err = s.root.Rename(tmpName, path); err != nil {
_ = s.root.Remove(tmpName)
return fmt.Errorf("failed to move temporary file: %w", err)
}
return nil
}
func (s *filesystemStorage) Open(_ context.Context, path string) (io.ReadCloser, int64, error) {
path = filepath.FromSlash(path)
file, err := s.root.Open(path)
if err != nil {
return nil, 0, err
}
info, err := file.Stat()
if err != nil {
file.Close()
return nil, 0, err
}
return file, info.Size(), nil
}
func (s *filesystemStorage) Delete(_ context.Context, path string) error {
path = filepath.FromSlash(path)
err := s.root.Remove(path)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
return nil
}
func (s *filesystemStorage) DeleteAll(_ context.Context, path string) error {
path = filepath.FromSlash(path)
// If "/", "." or "" is requested, we delete all contents of the root.
if path == "" || path == "/" || path == "." {
dir, err := s.root.Open(".")
if err != nil {
return fmt.Errorf("failed to open root directory: %w", err)
}
defer dir.Close()
entries, err := dir.ReadDir(-1)
if err != nil {
return fmt.Errorf("failed to list root directory: %w", err)
}
for _, entry := range entries {
if err := s.root.RemoveAll(entry.Name()); err != nil {
return fmt.Errorf("failed to delete '%s': %w", entry.Name(), err)
}
}
return nil
}
return s.root.RemoveAll(path)
}
func (s *filesystemStorage) List(_ context.Context, path string) ([]ObjectInfo, error) {
path = filepath.FromSlash(path)
dir, err := s.root.Open(path)
if err != nil {
return nil, err
}
defer dir.Close()
entries, err := dir.ReadDir(-1)
if err != nil {
return nil, err
}
objects := make([]ObjectInfo, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
info, err := entry.Info()
if err != nil {
return nil, err
}
objects = append(objects, ObjectInfo{
Path: filepath.Join(path, entry.Name()),
Size: info.Size(),
ModTime: info.ModTime(),
})
}
return objects, nil
}
func (s *filesystemStorage) Walk(_ context.Context, root string, fn func(ObjectInfo) error) error {
root = filepath.FromSlash(root)
fullPath := filepath.Clean(filepath.Join(s.absoluteRootPath, root))
// As we can't use os.Root here, we manually ensure that the fullPath is within the root directory
sep := string(filepath.Separator)
if !strings.HasPrefix(fullPath+sep, s.absoluteRootPath+sep) {
return fmt.Errorf("invalid root path: %s", root)
}
return filepath.WalkDir(fullPath, func(full string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
rel, err := filepath.Rel(s.absoluteRootPath, full)
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
return fn(ObjectInfo{
Path: filepath.ToSlash(rel),
Size: info.Size(),
ModTime: info.ModTime(),
})
})
}

View File

@@ -0,0 +1,68 @@
package storage
import (
"bytes"
"context"
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFilesystemStorageOperations(t *testing.T) {
ctx := context.Background()
store, err := NewFilesystemStorage(t.TempDir())
require.NoError(t, err)
t.Run("save, open and list files", func(t *testing.T) {
err := store.Save(ctx, "images/logo.png", bytes.NewBufferString("logo-data"))
require.NoError(t, err)
reader, size, err := store.Open(ctx, "images/logo.png")
require.NoError(t, err)
defer reader.Close()
contents, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, []byte("logo-data"), contents)
assert.Equal(t, int64(len(contents)), size)
err = store.Save(ctx, "images/nested/child.txt", bytes.NewBufferString("child"))
require.NoError(t, err)
files, err := store.List(ctx, "images")
require.NoError(t, err)
require.Len(t, files, 1)
assert.Equal(t, filepath.Join("images", "logo.png"), files[0].Path)
assert.Equal(t, int64(len("logo-data")), files[0].Size)
})
t.Run("delete files individually and idempotently", func(t *testing.T) {
err := store.Save(ctx, "images/delete-me.txt", bytes.NewBufferString("temp"))
require.NoError(t, err)
require.NoError(t, store.Delete(ctx, "images/delete-me.txt"))
_, _, err = store.Open(ctx, "images/delete-me.txt")
require.Error(t, err)
assert.True(t, IsNotExist(err))
// Deleting a missing object should be a no-op.
require.NoError(t, store.Delete(ctx, "images/missing.txt"))
})
t.Run("delete all files under a prefix", func(t *testing.T) {
require.NoError(t, store.Save(ctx, "images/a.txt", bytes.NewBufferString("a")))
require.NoError(t, store.Save(ctx, "images/b.txt", bytes.NewBufferString("b")))
require.NoError(t, store.DeleteAll(ctx, "images"))
_, _, err := store.Open(ctx, "images/a.txt")
require.Error(t, err)
assert.True(t, IsNotExist(err))
_, _, err = store.Open(ctx, "images/b.txt")
require.Error(t, err)
assert.True(t, IsNotExist(err))
})
}

View File

@@ -0,0 +1,185 @@
package storage
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"path/filepath"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
)
type S3Config struct {
Bucket string
Region string
Endpoint string
AccessKeyID string
SecretAccessKey string
ForcePathStyle bool
Root string
}
type s3Storage struct {
client *s3.Client
bucket string
prefix string
}
func NewS3Storage(ctx context.Context, cfg S3Config) (FileStorage, error) {
creds := credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, "")
awsCfg, err := awscfg.LoadDefaultConfig(ctx, awscfg.WithRegion(cfg.Region), awscfg.WithCredentialsProvider(creds))
if err != nil {
return nil, fmt.Errorf("failed to load AWS configuration: %w", err)
}
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
if cfg.Endpoint != "" {
o.BaseEndpoint = aws.String(cfg.Endpoint)
}
o.UsePathStyle = cfg.ForcePathStyle
})
return &s3Storage{
client: client,
bucket: cfg.Bucket,
prefix: strings.Trim(cfg.Root, "/"),
}, nil
}
func (s *s3Storage) Type() string {
return TypeS3
}
func (s *s3Storage) Save(ctx context.Context, path string, data io.Reader) error {
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(s.buildObjectKey(path)),
Body: data,
})
return err
}
func (s *s3Storage) Open(ctx context.Context, path string) (io.ReadCloser, int64, error) {
resp, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(s.buildObjectKey(path)),
})
if err != nil {
if isS3NotFound(err) {
return nil, 0, fs.ErrNotExist
}
return nil, 0, err
}
return resp.Body, aws.ToInt64(resp.ContentLength), nil
}
func (s *s3Storage) Delete(ctx context.Context, path string) error {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(s.buildObjectKey(path)),
})
return err
}
func (s *s3Storage) DeleteAll(ctx context.Context, path string) error {
paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{
Bucket: aws.String(s.bucket),
Prefix: aws.String(s.buildObjectKey(path)),
})
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return err
}
if len(page.Contents) == 0 {
continue
}
objects := make([]s3types.ObjectIdentifier, 0, len(page.Contents))
for _, obj := range page.Contents {
objects = append(objects, s3types.ObjectIdentifier{Key: obj.Key})
}
_, err = s.client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: aws.String(s.bucket),
Delete: &s3types.Delete{Objects: objects, Quiet: aws.Bool(true)},
})
if err != nil {
return err
}
}
return nil
}
func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error) {
paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{
Bucket: aws.String(s.bucket),
Prefix: aws.String(s.buildObjectKey(path)),
})
var objects []ObjectInfo
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, err
}
for _, obj := range page.Contents {
if obj.Key == nil {
continue
}
objects = append(objects, ObjectInfo{
Path: aws.ToString(obj.Key),
Size: aws.ToInt64(obj.Size),
ModTime: aws.ToTime(obj.LastModified),
})
}
}
return objects, nil
}
func (s *s3Storage) Walk(ctx context.Context, root string, fn func(ObjectInfo) error) error {
objects, err := s.List(ctx, root)
if err != nil {
return err
}
for _, obj := range objects {
if err := fn(obj); err != nil {
return err
}
}
return nil
}
func (s *s3Storage) buildObjectKey(p string) string {
p = filepath.Clean(p)
p = filepath.ToSlash(p)
p = strings.Trim(p, "/")
if p == "" || p == "." {
return s.prefix
}
if s.prefix == "" {
return p
}
return s.prefix + "/" + p
}
func isS3NotFound(err error) bool {
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
if apiErr.ErrorCode() == "NotFound" || apiErr.ErrorCode() == "NoSuchKey" {
return true
}
}
var missingKey *s3types.NoSuchKey
return errors.As(err, &missingKey)
}

View File

@@ -0,0 +1,44 @@
package storage
import (
"errors"
"testing"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"github.com/stretchr/testify/assert"
)
func TestS3Helpers(t *testing.T) {
t.Run("buildObjectKey trims and joins prefix", func(t *testing.T) {
tests := []struct {
name string
prefix string
path string
expected string
}{
{name: "no prefix no path", prefix: "", path: "", expected: ""},
{name: "prefix no path", prefix: "root", path: "", expected: "root"},
{name: "prefix with nested path", prefix: "root", path: "foo/bar/baz", expected: "root/foo/bar/baz"},
{name: "trimmed path and prefix", prefix: "root", path: "/foo//bar/", expected: "root/foo/bar"},
{name: "no prefix path only", prefix: "", path: "./images/logo.png", expected: "images/logo.png"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s := &s3Storage{
bucket: "bucket",
prefix: tc.prefix,
}
assert.Equal(t, tc.expected, s.buildObjectKey(tc.path))
})
}
})
t.Run("isS3NotFound detects expected errors", func(t *testing.T) {
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NoSuchKey"}))
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NotFound"}))
assert.True(t, isS3NotFound(&s3types.NoSuchKey{}))
assert.False(t, isS3NotFound(errors.New("boom")))
})
}

View File

@@ -0,0 +1,33 @@
package storage
import (
"context"
"io"
"os"
"time"
)
var (
TypeFileSystem = "fs"
TypeS3 = "s3"
)
type ObjectInfo struct {
Path string
Size int64
ModTime time.Time
}
type FileStorage interface {
Save(ctx context.Context, relativePath string, data io.Reader) error
Open(ctx context.Context, relativePath string) (io.ReadCloser, int64, error)
Delete(ctx context.Context, relativePath string) error
DeleteAll(ctx context.Context, prefix string) error
List(ctx context.Context, prefix string) ([]ObjectInfo, error)
Walk(ctx context.Context, root string, fn func(ObjectInfo) error) error
Type() string
}
func IsNotExist(err error) bool {
return os.IsNotExist(err)
}

View File

@@ -2,20 +2,15 @@ package utils
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"mime" "mime"
"mime/multipart"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
"github.com/google/uuid"
"github.com/pocket-id/pocket-id/backend/resources"
) )
func GetFileExtension(filename string) string { func GetFileExtension(filename string) string {
@@ -86,110 +81,6 @@ func GetImageExtensionFromMimeType(mimeType string) string {
} }
} }
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
srcFile, err := resources.FS.Open(srcFilePath)
if err != nil {
return fmt.Errorf("failed to open embedded file: %w", err)
}
defer srcFile.Close()
err = os.MkdirAll(filepath.Dir(destFilePath), os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
destFile, err := os.Create(destFilePath)
if err != nil {
return fmt.Errorf("failed to open destination file: %w", err)
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return fmt.Errorf("failed to write to destination file: %w", err)
}
return nil
}
func EmbeddedFileSha256(filePath string) ([]byte, error) {
f, err := resources.FS.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open embedded file: %w", err)
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return nil, fmt.Errorf("failed to read embedded file: %w", err)
}
return h.Sum(nil), nil
}
func SaveFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0o750); err != nil {
return err
}
return SaveFileStream(src, dst)
}
// SaveFileStream saves a stream to a file.
func SaveFileStream(r io.Reader, dstFileName string) error {
// Our strategy is to save to a separate file and then rename it to override the original file
tmpFileName := dstFileName + "." + uuid.NewString() + "-tmp"
// Write to the temporary file
tmpFile, err := os.Create(tmpFileName)
if err != nil {
return fmt.Errorf("failed to open file '%s' for writing: %w", tmpFileName, err)
}
n, err := io.Copy(tmpFile, r)
if err != nil {
// Delete the temporary file; we ignore errors here
_ = tmpFile.Close()
_ = os.Remove(tmpFileName)
return fmt.Errorf("failed to write to file '%s': %w", tmpFileName, err)
}
err = tmpFile.Close()
if err != nil {
// Delete the temporary file; we ignore errors here
_ = os.Remove(tmpFileName)
return fmt.Errorf("failed to close stream to file '%s': %w", tmpFileName, err)
}
if n == 0 {
// Delete the temporary file; we ignore errors here
_ = os.Remove(tmpFileName)
return errors.New("no data written")
}
// Rename to the final file, which overrides existing files
// This is an atomic operation
err = os.Rename(tmpFileName, dstFileName)
if err != nil {
// Delete the temporary file; we ignore errors here
_ = os.Remove(tmpFileName)
return fmt.Errorf("failed to rename file '%s': %w", dstFileName, err)
}
return nil
}
// FileExists returns true if a file exists on disk and is a regular file // FileExists returns true if a file exists on disk and is a regular file
func FileExists(path string) (bool, error) { func FileExists(path string) (bool, error) {
s, err := os.Stat(path) s, err := os.Stat(path)

View File

@@ -19,7 +19,7 @@ import (
const profilePictureSize = 300 const profilePictureSize = 300
// CreateProfilePicture resizes the profile picture to a square // CreateProfilePicture resizes the profile picture to a square
func CreateProfilePicture(file io.Reader) (io.Reader, error) { func CreateProfilePicture(file io.Reader) (io.ReadSeeker, error) {
img, _, err := imageorient.Decode(file) img, _, err := imageorient.Decode(file)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode image: %w", err) return nil, fmt.Errorf("failed to decode image: %w", err)
@@ -27,17 +27,13 @@ func CreateProfilePicture(file io.Reader) (io.Reader, error) {
img = imaging.Fill(img, profilePictureSize, profilePictureSize, imaging.Center, imaging.Lanczos) img = imaging.Fill(img, profilePictureSize, profilePictureSize, imaging.Center, imaging.Lanczos)
pr, pw := io.Pipe() var buf bytes.Buffer
go func() { err = imaging.Encode(&buf, img, imaging.PNG)
innerErr := imaging.Encode(pw, img, imaging.PNG) if err != nil {
if innerErr != nil { return nil, fmt.Errorf("failed to encode image: %w", err)
_ = pw.CloseWithError(fmt.Errorf("failed to encode image: %w", innerErr))
return
} }
pw.Close()
}()
return pr, nil return bytes.NewReader(buf.Bytes()), nil
} }
// CreateDefaultProfilePicture creates a profile picture with the initials // CreateDefaultProfilePicture creates a profile picture with the initials

View File

@@ -1,3 +1 @@
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"> {{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><!--html--><!--head--><!--body--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"><img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">API Key Expiring Soon</h1></td><td align="right" data-id="__react-email-column"><p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Hello <!-- -->{{.Data.Name}}<!-- -->, <br/>This is a reminder that your API key <strong>{{.Data.APIKeyName}}</strong> <!-- -->will expire on <strong>{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}</strong>.</p><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Please generate a new API key if you need continued access.</p></div></td></tr></tbody></table><!--/$--></body></html>{{end}}
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">API Key Expiring Soon</h1></td><td align="right" data-id="__react-email-column">
<p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Hello <!-- -->{{.Data.Name}}<!-- -->, <br/>This is a reminder that your API key <strong>{{.Data.APIKeyName}}</strong> <!-- -->will expire on <strong>{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}</strong>.</p><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Please generate a new API key if you need continued access.</p></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}

View File

@@ -1,5 +1 @@
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"> {{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><!--html--><!--head--><!--body--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"><img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">New Sign-In Detected</h1></td><td align="right" data-id="__react-email-column"><p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your <!-- -->{{.AppName}}<!-- --> account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.</p><h4 style="font-size:1rem;font-weight:bold;margin:30px 0 10px 0">Details</h4><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Approximate Location</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">IP Address</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.IPAddress}}</p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:10px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Device</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.Device}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Sign-In Time</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}</p></td></tr></tbody></table></div></td></tr></tbody></table><!--/$--></body></html>{{end}}
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">New Sign-In Detected</h1></td><td align="right" data-id="__react-email-column">
<p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your <!-- -->{{.AppName}}<!-- --> account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.</p><h4 style="font-size:1rem;font-weight:bold;margin:30px 0 10px 0">Details</h4><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Approximate Location</p>
<p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">IP Address</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.IPAddress}}</p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:10px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Device</p>
<p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.Device}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Sign-In Time</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}</p></td></tr></tbody></table></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}

View File

@@ -1,4 +1 @@
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"> {{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><!--html--><!--head--><!--body--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"><img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Your Login Code</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Click the button below to sign in to <!-- -->{{.AppName}}<!-- --> with a login code.<br/>Or visit<!-- --> <a href="{{.Data.LoginLink}}" style="color:#000;text-decoration-line:none;text-decoration:underline;font-family:Arial, sans-serif" target="_blank">{{.Data.LoginLink}}</a> <!-- -->and enter the code <strong>{{.Data.Code}}</strong>.<br/><br/>This code expires in <!-- -->{{.Data.ExpirationString}}<!-- -->.</p><div style="text-align:center"><a href="{{.Data.LoginLinkWithCode}}" style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#000000;color:#ffffff;padding:12px 24px;border-radius:4px;font-size:15px;font-weight:500;cursor:pointer;margin-top:10px;padding-top:12px;padding-right:24px;padding-bottom:12px;padding-left:24px" target="_blank"><span><!--[if mso]><i style="mso-font-width:400%;mso-text-raise:18" hidden>&#8202;&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Sign In</span><span><!--[if mso]><i style="mso-font-width:400%" hidden>&#8202;&#8202;&#8202;&#8203;</i><![endif]--></span></a></div></div></td></tr></tbody></table><!--/$--></body></html>{{end}}
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Your Login Code</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Click the button below to sign in to <!-- -->
{{.AppName}}<!-- --> with a login code.<br/>Or visit<!-- --> <a href="{{.Data.LoginLink}}" style="color:#000;text-decoration-line:none;text-decoration:underline;font-family:Arial, sans-serif" target="_blank">{{.Data.LoginLink}}</a> <!-- -->and enter the code <strong>{{.Data.Code}}</strong>.<br/><br/>This code expires in <!-- -->{{.Data.ExpirationString}}<!-- -->.</p><div style="text-align:center"><a href="{{.Data.LoginLinkWithCode}}" style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#000000;color:#ffffff;padding:12px 24px;border-radius:4px;font-size:15px;font-weight:500;cursor:pointer;margin-top:10px;padding-top:12px;padding-right:24px;padding-bottom:12px;padding-left:24px" target="_blank"><span><!--[if mso]><i style="mso-font-width:400%;mso-text-raise:18" hidden>&#8202;&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">
Sign In</span><span><!--[if mso]><i style="mso-font-width:400%" hidden>&#8202;&#8202;&#8202;&#8203;</i><![endif]--></span></a></div></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}

View File

@@ -1,3 +1 @@
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"> {{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><!--html--><!--head--><!--body--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"><img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Test Email</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your email setup is working correctly!</p></div></td></tr></tbody></table><!--/$--></body></html>{{end}}
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Test Email</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your email setup is working correctly!</p></div></td>
</tr></tbody></table><!--7--><!--/$--></body></html>{{end}}

View File

@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_api_keys_key;
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_token;
DROP INDEX IF EXISTS idx_signup_tokens_token;
DROP INDEX IF EXISTS idx_reauthentication_tokens_token;

View File

@@ -12,40 +12,6 @@ function getTemplateName(filename: string): string {
return filename.replace(".tsx", ""); return filename.replace(".tsx", "");
} }
/**
* Tag-aware wrapping:
* - Prefer breaking immediately after the last '>' within maxLen.
* - Never break at spaces.
* - If no '>' exists in the window, hard-break at maxLen.
*/
function tagAwareWrap(input: string, maxLen: number): string {
const out: string[] = [];
for (const originalLine of input.split(/\r?\n/)) {
let line = originalLine;
while (line.length > maxLen) {
let breakPos = line.lastIndexOf(">", maxLen);
// If '>' happens to be exactly at maxLen, break after it
if (breakPos === maxLen) breakPos = maxLen;
// If we found a '>' before the limit, break right after it
if (breakPos > -1 && breakPos < maxLen) {
out.push(line.slice(0, breakPos + 1));
line = line.slice(breakPos + 1);
continue;
}
// No suitable tag end found—hard break
out.push(line.slice(0, maxLen));
line = line.slice(maxLen);
}
out.push(line);
}
return out.join("\n");
}
async function buildTemplateFile( async function buildTemplateFile(
Component: any, Component: any,
templateName: string, templateName: string,
@@ -58,11 +24,7 @@ async function buildTemplateFile(
// Normalize quotes // Normalize quotes
const normalized = rendered.replace(/&quot;/g, '"'); const normalized = rendered.replace(/&quot;/g, '"');
// Enforce line length: prefer tag boundaries, never spaces const goTemplate = `{{define "root"}}${normalized}{{end}}`;
const maxLen = isPlainText ? 78 : 998; // RFC-safe
const safe = tagAwareWrap(normalized, maxLen);
const goTemplate = `{{define "root"}}${safe}{{end}}`;
const suffix = isPlainText ? "_text.tmpl" : "_html.tmpl"; const suffix = isPlainText ? "_text.tmpl" : "_html.tmpl";
const templatePath = path.join(outputDir, `${templateName}${suffix}`); const templatePath = path.join(outputDir, `${templateName}${suffix}`);

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Jste si jisti, že chcete zrušit klíč API \"{apiKeyName}\"? To naruší všechny integrace pomocí tohoto klíče.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Jste si jisti, že chcete zrušit klíč API \"{apiKeyName}\"? To naruší všechny integrace pomocí tohoto klíče.",
"last_used": "Naposledy použito", "last_used": "Naposledy použito",
"actions": "Akce", "actions": "Akce",
"images_updated_successfully": "Obrázky úspěšně aktualizovány", "images_updated_successfully": "Obrázky byly úspěšně aktualizovány. Aktualizace může trvat několik minut.",
"general": "Obecné", "general": "Obecné",
"configure_smtp_to_send_emails": "Povolte e-mailová oznámení pro upozornění uživatelů, pokud je zjištěno přihlášení z nového zařízení nebo lokace.", "configure_smtp_to_send_emails": "Povolte e-mailová oznámení pro upozornění uživatelů, pokud je zjištěno přihlášení z nového zařízení nebo lokace.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -456,10 +456,14 @@
"invalid_url": "Neplatná adresa URL", "invalid_url": "Neplatná adresa URL",
"require_user_email": "Vyžadovat e-mailovou adresu", "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", "view": "Zobrazení",
"toggle_columns": "Přepínat sloupce", "toggle_columns": "Přepnout sloupce",
"locale": "Místní nastavení", "locale": "Jazyk",
"ldap_id": "LDAP ID", "ldap_id": "LDAP ID",
"reauthentication": "Opětovné ověření", "reauthentication": "Opětovné ověření",
"clear_filters": "Vymazat filtry" "clear_filters": "Vymazat filtry",
"default_profile_picture": "Výchozí profilový obrázek",
"light": "Světlo",
"dark": "Tmavá",
"system": "Systém"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Er du sikker på, at du vil tilbagekalde API-nøglen \"{apiKeyName}\"? Dette vil afbryde alle integrationer, der bruger nøglen.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Er du sikker på, at du vil tilbagekalde API-nøglen \"{apiKeyName}\"? Dette vil afbryde alle integrationer, der bruger nøglen.",
"last_used": "Sidst brugt", "last_used": "Sidst brugt",
"actions": "Handlinger", "actions": "Handlinger",
"images_updated_successfully": "Billeder blev opdateret", "images_updated_successfully": "Billeder opdateret. Det kan tage et par minutter at opdatere.",
"general": "Generelt", "general": "Generelt",
"configure_smtp_to_send_emails": "Aktivér e-mailnotifikationer for at advare brugere, når et login registreres fra en ny enhed eller placering.", "configure_smtp_to_send_emails": "Aktivér e-mailnotifikationer for at advare brugere, når et login registreres fra en ny enhed eller placering.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Lokale", "locale": "Lokale",
"ldap_id": "LDAP-id", "ldap_id": "LDAP-id",
"reauthentication": "Genbekræftelse", "reauthentication": "Genbekræftelse",
"clear_filters": "Ryd filtre" "clear_filters": "Ryd filtre",
"default_profile_picture": "Standardprofilbillede",
"light": "Lys",
"dark": "Mørk",
"system": "System"
} }

View File

@@ -17,7 +17,7 @@
"image_should_be_in_format": "Das Bild sollte im PNG- oder JPEG-Format vorliegen.", "image_should_be_in_format": "Das Bild sollte im PNG- oder JPEG-Format vorliegen.",
"items_per_page": "Einträge pro Seite", "items_per_page": "Einträge pro Seite",
"no_items_found": "Keine Einträge gefunden", "no_items_found": "Keine Einträge gefunden",
"select_items": "Wähle Artikel aus...", "select_items": "Elemente auswählen...",
"search": "Suchen...", "search": "Suchen...",
"expand_card": "Karte erweitern", "expand_card": "Karte erweitern",
"copied": "Kopiert", "copied": "Kopiert",
@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bist du sicher, dass du den API-Schlüssel \"{apiKeyName}\" widerrufen willst? Das wird jegliche Integrationen, die diesen Schlüssel verwenden, brechen.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bist du sicher, dass du den API-Schlüssel \"{apiKeyName}\" widerrufen willst? Das wird jegliche Integrationen, die diesen Schlüssel verwenden, brechen.",
"last_used": "Letzte Verwendung", "last_used": "Letzte Verwendung",
"actions": "Aktionen", "actions": "Aktionen",
"images_updated_successfully": "Bild erfolgreich aktualisiert", "images_updated_successfully": "Bilder erfolgreich aktualisiert. Die Aktualisierung kann ein paar Minuten dauern.",
"general": "Allgemein", "general": "Allgemein",
"configure_smtp_to_send_emails": "Aktiviere E-Mail Benachrichtigungen, um Benutzer zu informieren, wenn ein Login von einem neuen Gerät oder Standort erkannt wird.", "configure_smtp_to_send_emails": "Aktiviere E-Mail Benachrichtigungen, um Benutzer zu informieren, wenn ein Login von einem neuen Gerät oder Standort erkannt wird.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Ort", "locale": "Ort",
"ldap_id": "LDAP-ID", "ldap_id": "LDAP-ID",
"reauthentication": "Erneute Authentifizierung", "reauthentication": "Erneute Authentifizierung",
"clear_filters": "Filter löschen" "clear_filters": "Filter löschen",
"default_profile_picture": "Standard-Profilbild",
"light": "Hell",
"dark": "Dunkel",
"system": "System"
} }

View File

@@ -155,7 +155,7 @@
"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.", "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", "last_used": "Last Used",
"actions": "Actions", "actions": "Actions",
"images_updated_successfully": "Images updated successfully", "images_updated_successfully": "Images updated successfully. It may take a few minutes to update.",
"general": "General", "general": "General",
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.", "configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Locale", "locale": "Locale",
"ldap_id": "LDAP ID", "ldap_id": "LDAP ID",
"reauthentication": "Re-authentication", "reauthentication": "Re-authentication",
"clear_filters" : "Clear Filters" "clear_filters": "Clear Filters",
"default_profile_picture": "Default Profile Picture",
"light": "Light",
"dark": "Dark",
"system": "System"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "¿Estás seguro de que deseas invalidar la API Key \"{apiKeyName}\"? Esto romperá cualquier integración que esté usando esta clave.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "¿Estás seguro de que deseas invalidar la API Key \"{apiKeyName}\"? Esto romperá cualquier integración que esté usando esta clave.",
"last_used": "Utilizado por última vez", "last_used": "Utilizado por última vez",
"actions": "Acciones", "actions": "Acciones",
"images_updated_successfully": "Imágenes actualizadas correctamente", "images_updated_successfully": "Imágenes actualizadas correctamente. La actualización puede tardar unos minutos.",
"general": "General", "general": "General",
"configure_smtp_to_send_emails": "Habilita las notificaciones por correo electrónico para alertar a los usuarios cuando se detecta un inicio de sesión desde un nuevo dispositivo o ubicación.", "configure_smtp_to_send_emails": "Habilita las notificaciones por correo electrónico para alertar a los usuarios cuando se detecta un inicio de sesión desde un nuevo dispositivo o ubicación.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Configuración regional", "locale": "Configuración regional",
"ldap_id": "Identificador LDAP", "ldap_id": "Identificador LDAP",
"reauthentication": "Reautenticación", "reauthentication": "Reautenticación",
"clear_filters": "Borrar filtros" "clear_filters": "Borrar filtros",
"default_profile_picture": "Imagen de perfil predeterminada",
"light": "Luz",
"dark": "Oscuro",
"system": "Sistema"
} }

View File

@@ -17,14 +17,14 @@
"image_should_be_in_format": "L'image doit être au format PNG ou JPEG.", "image_should_be_in_format": "L'image doit être au format PNG ou JPEG.",
"items_per_page": "Éléments par page", "items_per_page": "Éléments par page",
"no_items_found": "Aucune donnée trouvée", "no_items_found": "Aucune donnée trouvée",
"select_items": "Choisis des trucs...", "select_items": "Sélectionner des éléments...",
"search": "Rechercher...", "search": "Rechercher...",
"expand_card": "Carte d'expansion", "expand_card": "Développer la carte",
"copied": "Copié", "copied": "Copié",
"click_to_copy": "Cliquer pour copier", "click_to_copy": "Cliquer pour copier",
"something_went_wrong": "Quelque chose n'a pas fonctionné", "something_went_wrong": "Quelque chose n'a pas fonctionné",
"go_back_to_home": "Retourner à l'accueil", "go_back_to_home": "Retourner à l'accueil",
"alternative_sign_in_methods": "Autres façons de se connecter", "alternative_sign_in_methods": "Méthodes de connexion alternatives",
"login_background": "Arrière-plan de connexion", "login_background": "Arrière-plan de connexion",
"logo": "Logo", "logo": "Logo",
"login_code": "Code de connexion", "login_code": "Code de connexion",
@@ -75,10 +75,10 @@
"use_your_passkey_instead": "Utiliser votre clé d'accès à la place ?", "use_your_passkey_instead": "Utiliser votre clé d'accès à la place ?",
"email_login": "Connexion par e-mail", "email_login": "Connexion par e-mail",
"enter_a_login_code_to_sign_in": "Entrez un code de connexion pour vous connecter.", "enter_a_login_code_to_sign_in": "Entrez un code de connexion pour vous connecter.",
"sign_in_with_login_code": "Connecte-toi avec ton code d'accès", "sign_in_with_login_code": "Connectez-vous avec votre code d'accès",
"request_a_login_code_via_email": "Demander un code de connexion par e-mail.", "request_a_login_code_via_email": "Demander un code de connexion par e-mail.",
"go_back": "Retour", "go_back": "Retour",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Un e-mail a été envoyé à l'e-mail mentionné, si elle existe dans le système.", "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Un e-mail a été envoyé à l'adresse mentionnée, si celle-ci existe dans le système.",
"enter_code": "Entrez le code", "enter_code": "Entrez le code",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Entrez votre adresse e-mail pour recevoir un email avec un code de connexion.", "enter_your_email_address_to_receive_an_email_with_a_login_code": "Entrez votre adresse e-mail pour recevoir un email avec un code de connexion.",
"your_email": "Votre email", "your_email": "Votre email",
@@ -90,10 +90,10 @@
"users": "Utilisateurs", "users": "Utilisateurs",
"user_groups": "Groupes dutilisateurs", "user_groups": "Groupes dutilisateurs",
"oidc_clients": "Clients OIDC", "oidc_clients": "Clients OIDC",
"api_keys": "Clés API", "api_keys": "Clés d'API",
"application_configuration": "Configuration de lapplication", "application_configuration": "Configuration de lapplication",
"settings": "Paramètres", "settings": "Paramètres",
"update_pocket_id": "Mise à jour de Pocket ID", "update_pocket_id": "Mettre à jour Pocket ID",
"powered_by": "Propulsé par", "powered_by": "Propulsé par",
"see_your_account_activities_from_the_last_3_months": "Consultez les activités de votre compte au cours des 3 derniers mois.", "see_your_account_activities_from_the_last_3_months": "Consultez les activités de votre compte au cours des 3 derniers mois.",
"time": "Date et heure", "time": "Date et heure",
@@ -139,7 +139,7 @@
"add_api_key": "Crée une clé API", "add_api_key": "Crée une clé API",
"manage_api_keys": "Gérer les clés API", "manage_api_keys": "Gérer les clés API",
"api_key_created": "Clé API créée", "api_key_created": "Clé API créée",
"for_security_reasons_this_key_will_only_be_shown_once": "Pour des raisons de sécurité, cette clé ne sera affichée qu'une seule fois. Veuillez la conserver en toute sécurité.", "for_security_reasons_this_key_will_only_be_shown_once": "Pour des raisons de sécurité, cette clé ne sera affichée qu'une seule fois. Veuillez la conserver dans un endroit sûr.",
"description": "Description", "description": "Description",
"api_key": "Clé API", "api_key": "Clé API",
"close": "Fermer", "close": "Fermer",
@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Êtes-vous sûr de vouloir révoquer la clé API \"{apiKeyName}\" ? Cela va casser toutes les intégrations utilisant cette clé.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Êtes-vous sûr de vouloir révoquer la clé API \"{apiKeyName}\" ? Cela va casser toutes les intégrations utilisant cette clé.",
"last_used": "Dernière utilisation", "last_used": "Dernière utilisation",
"actions": "Actions", "actions": "Actions",
"images_updated_successfully": "Image mise à jour avec succès", "images_updated_successfully": "Les images ont été mises à jour sans problème. Ça peut prendre quelques minutes pour que tout se mette à jour.",
"general": "Général", "general": "Général",
"configure_smtp_to_send_emails": "Activer les notifications par e-mail pour alerter les utilisateurs lorsqu'une connexion est détectée à partir d'un nouvel appareil ou d'un nouvel emplacement.", "configure_smtp_to_send_emails": "Activer les notifications par e-mail pour alerter les utilisateurs lorsqu'une connexion est détectée à partir d'un nouvel appareil ou d'un nouvel emplacement.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -163,7 +163,7 @@
"images": "Images", "images": "Images",
"update": "Mise à jour", "update": "Mise à jour",
"email_configuration_updated_successfully": "La configuration du serveur mail à été mise à jour avec succès", "email_configuration_updated_successfully": "La configuration du serveur mail à été mise à jour avec succès",
"save_changes_question": "Enregistrer des changements?", "save_changes_question": "Enregistrer ces changements?",
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Vous devez enregistrer les modifications avant d'envoyer un e-mail de test. Voulez-vous enregistrer maintenant ?", "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Vous devez enregistrer les modifications avant d'envoyer un e-mail de test. Voulez-vous enregistrer maintenant ?",
"save_and_send": "Enregistrer et envoyer", "save_and_send": "Enregistrer et envoyer",
"test_email_sent_successfully": "L'e-mail de test a été envoyé avec succès à votre adresse e-mail.", "test_email_sent_successfully": "L'e-mail de test a été envoyé avec succès à votre adresse e-mail.",
@@ -256,7 +256,7 @@
"manage_user_groups": "Gérer les groupes d'utilisateurs", "manage_user_groups": "Gérer les groupes d'utilisateurs",
"friendly_name": "Nom d'affichage", "friendly_name": "Nom d'affichage",
"name_that_will_be_displayed_in_the_ui": "Nom qui sera affiché dans l'interface utilisateur", "name_that_will_be_displayed_in_the_ui": "Nom qui sera affiché dans l'interface utilisateur",
"name_that_will_be_in_the_groups_claim": "Nommez ce qui sera dans le \"groupe\" claim", "name_that_will_be_in_the_groups_claim": "Nom qui sera dans la revendication \"groups\"",
"delete_name": "Supprimer {name}", "delete_name": "Supprimer {name}",
"are_you_sure_you_want_to_delete_this_user_group": "Êtes-vous sûr de vouloir supprimer ce groupe d'utilisateurs?", "are_you_sure_you_want_to_delete_this_user_group": "Êtes-vous sûr de vouloir supprimer ce groupe d'utilisateurs?",
"user_group_deleted_successfully": "Groupe d'utilisateurs supprimé avec succès", "user_group_deleted_successfully": "Groupe d'utilisateurs supprimé avec succès",
@@ -272,14 +272,14 @@
"add_oidc_client": "Ajouter un client OIDC", "add_oidc_client": "Ajouter un client OIDC",
"manage_oidc_clients": "Gérer les clients OIDC", "manage_oidc_clients": "Gérer les clients OIDC",
"one_time_link": "Lien de connexion unique", "one_time_link": "Lien de connexion unique",
"use_this_link_to_sign_in_once": "Utilisez ce lien pour vous connecter. Ceci est nécessaire pour les utilisateurs qui n'ont pas encore ajouté de clé d'accès ou l'ont perdu.", "use_this_link_to_sign_in_once": "Utilisez ce lien pour vous connecter. Ceci est nécessaire pour les utilisateurs qui n'ont pas encore ajouté de clé d'accès ou qui l'ont perdu.",
"add": "Ajouter", "add": "Ajouter",
"callback_urls": "URL de callback", "callback_urls": "URL de callback",
"logout_callback_urls": "URL de callback de déconnexion", "logout_callback_urls": "URL de callback de déconnexion",
"public_client": "Client public", "public_client": "Client public",
"public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.", "public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et linterception de code dautorisation.", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et dinterception de code dautorisation.",
"requires_reauthentication": "Nécessite une nouvelle authentification", "requires_reauthentication": "Nécessite une nouvelle authentification",
"requires_users_to_authenticate_again_on_each_authorization": "Demande aux utilisateurs de se connecter à nouveau à chaque autorisation, même s'ils sont déjà connectés.", "requires_users_to_authenticate_again_on_each_authorization": "Demande aux utilisateurs de se connecter à nouveau à chaque autorisation, même s'ils sont déjà connectés.",
"name_logo": "Logo {name}", "name_logo": "Logo {name}",
@@ -318,7 +318,7 @@
"reset": "Réinitialiser", "reset": "Réinitialiser",
"reset_to_default": "Valeurs par défaut", "reset_to_default": "Valeurs par défaut",
"profile_picture_has_been_reset": "La photo de profil a été réinitialisée. La mise à jour peut prendre quelques minutes.", "profile_picture_has_been_reset": "La photo de profil a été réinitialisée. La mise à jour peut prendre quelques minutes.",
"select_the_language_you_want_to_use": "Choisis la langue que tu veux utiliser. Attention, certains textes peuvent être traduits automatiquement et ne pas être tout à fait exacts.", "select_the_language_you_want_to_use": "Choisissez la langue que vous souhaitez utiliser. Attention, certains textes peuvent être traduits automatiquement et ne pas être tout à fait exacts.",
"contribute_to_translation": "Si vous trouvez un problème, n'hésitez pas à contribuer à la traduction sur <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.", "contribute_to_translation": "Si vous trouvez un problème, n'hésitez pas à contribuer à la traduction sur <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
"personal": "Personnel", "personal": "Personnel",
"global": "Global", "global": "Global",
@@ -348,7 +348,7 @@
"callback_url_description": "URL(s) fournies par votre client. Sera automatiquement ajoutée si laissée vide. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.", "callback_url_description": "URL(s) fournies par votre client. Sera automatiquement ajoutée si laissée vide. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
"logout_callback_url_description": "URL(s) fournies par votre client pour la déconnexion. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.", "logout_callback_url_description": "URL(s) fournies par votre client pour la déconnexion. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
"api_key_expiration": "Expiration de la clé API", "api_key_expiration": "Expiration de la clé API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envoyer un email à l'utilisateur lorsque sa clé API est sur le point d'expirer.", "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envoyer un email à l'utilisateur lorsque sa clé d'API est sur le point d'expirer.",
"authorize_device": "Autoriser l'appareil", "authorize_device": "Autoriser l'appareil",
"the_device_has_been_authorized": "L'appareil a été autorisé.", "the_device_has_been_authorized": "L'appareil a été autorisé.",
"enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.", "enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.",
@@ -382,7 +382,7 @@
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Sélectionnez une couleur d'accent pour personnaliser l'apparence de Pocket ID.", "select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Sélectionnez une couleur d'accent pour personnaliser l'apparence de Pocket ID.",
"accent_color": "Couleur d'accent", "accent_color": "Couleur d'accent",
"custom_accent_color": "Couleur d'accent personnalisée", "custom_accent_color": "Couleur d'accent personnalisée",
"custom_accent_color_description": "Entrez une couleur personnalisée en utilisant un format CSS valide (par ex. hex, rgb, hsl).", "custom_accent_color_description": "Entrez une couleur personnalisée en utilisant un format CSS valide (ex. : hex, rgb, hsl).",
"color_value": "Valeur de la couleur", "color_value": "Valeur de la couleur",
"apply": "Appliquer", "apply": "Appliquer",
"signup_token": "Jeton d'inscription", "signup_token": "Jeton d'inscription",
@@ -429,19 +429,19 @@
"signup_open": "Inscription ouverte", "signup_open": "Inscription ouverte",
"signup_open_description": "Toute personne peut créer un nouveau compte sans restriction.", "signup_open_description": "Toute personne peut créer un nouveau compte sans restriction.",
"of": "sur", "of": "sur",
"skip_passkey_setup": "Ignorer la configuration de la clé d'accès", "skip_passkey_setup": "Ignorer la configuration d'une clé d'accès",
"skip_passkey_setup_description": "Il est fortement recommandé de configurer une clé d'accès, car sans elle, vous serez verrouillé hors de votre compte dès l'expiration de la session.", "skip_passkey_setup_description": "Il est fortement recommandé de configurer une clé d'accès, car sans elle, vous serez verrouillé hors de votre compte dès l'expiration de la session.",
"my_apps": "Mes applications", "my_apps": "Mes applications",
"no_apps_available": "Aucune appli disponible", "no_apps_available": "Aucune app disponible",
"contact_your_administrator_for_app_access": "Contacte ton administrateur pour avoir accès aux applications.", "contact_your_administrator_for_app_access": "Contactez votre administrateur pour avoir accès aux applications.",
"launch": "Lancement", "launch": "Lancement",
"client_launch_url": "URL de lancement du client", "client_launch_url": "URL de lancement du client",
"client_launch_url_description": "L'URL qui s'ouvrira quand quelqu'un lancera l'appli depuis la page Mes applis.", "client_launch_url_description": "L'URL qui s'ouvrira quand quelqu'un lancera l'appli depuis la page Mes applis.",
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.", "client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
"revoke_access": "Supprimer l'accès", "revoke_access": "Supprimer l'accès",
"revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de ton compte.", "revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de votre compte.",
"revoke_access_successful": "L'accès à {clientName} a été supprimé.", "revoke_access_successful": "L'accès à {clientName} a été supprimé.",
"last_signed_in_ago": "Dernière connexion il y a {time} il y a", "last_signed_in_ago": "Dernière connexion il y a {time}",
"invalid_client_id": "L'ID client ne peut contenir que des lettres, des chiffres, des traits de soulignement et des tirets.", "invalid_client_id": "L'ID client ne peut contenir que des lettres, des chiffres, des traits de soulignement et des tirets.",
"custom_client_id_description": "Définissez un identifiant client personnalisé si votre application l'exige. Sinon, laissez ce champ vide pour qu'un identifiant aléatoire soit généré.", "custom_client_id_description": "Définissez un identifiant client personnalisé si votre application l'exige. Sinon, laissez ce champ vide pour qu'un identifiant aléatoire soit généré.",
"generated": "Généré", "generated": "Généré",
@@ -452,14 +452,18 @@
"configure_application_images": "Configurer les images d'application", "configure_application_images": "Configurer les images d'application",
"ui_config_disabled_info_title": "Configuration de l'interface utilisateur désactivée", "ui_config_disabled_info_title": "Configuration de l'interface utilisateur désactivée",
"ui_config_disabled_info_description": "La configuration de l'interface utilisateur est désactivée parce que les paramètres de configuration de l'application sont gérés par des variables d'environnement. Certains paramètres peuvent ne pas être modifiables.", "ui_config_disabled_info_description": "La configuration de l'interface utilisateur est désactivée parce que les paramètres de configuration de l'application sont gérés par des variables d'environnement. Certains paramètres peuvent ne pas être modifiables.",
"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>.", "logo_from_url_description": "Collez une URL pointant une image (svg, png, webp). Trouvez 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", "invalid_url": "URL invalide",
"require_user_email": "Besoin d'une adresse e-mail", "require_user_email": "Adresse e-mail requise",
"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 nécessitant un e-mail.",
"view": "Voir", "view": "Voir",
"toggle_columns": "Basculer les colonnes", "toggle_columns": "Basculer les colonnes",
"locale": "Paramètres régionaux", "locale": "Paramètres régionaux",
"ldap_id": "ID LDAP", "ldap_id": "ID LDAP",
"reauthentication": "Réauthentification", "reauthentication": "Réauthentification",
"clear_filters": "Effacer les filtres" "clear_filters": "Effacer les filtres",
"default_profile_picture": "Photo de profil par défaut",
"light": "Lumière",
"dark": "Sombre",
"system": "Système"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Sei sicuro di voler revocare la chiave API \"{apiKeyName}\"? Questo comprometterà qualsiasi integrazione che utilizza questa chiave.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Sei sicuro di voler revocare la chiave API \"{apiKeyName}\"? Questo comprometterà qualsiasi integrazione che utilizza questa chiave.",
"last_used": "Ultimo utilizzo", "last_used": "Ultimo utilizzo",
"actions": "Azioni", "actions": "Azioni",
"images_updated_successfully": "Immagini aggiornate con successo", "images_updated_successfully": "Immagini aggiornate senza problemi. Potrebbe volerci qualche minuto per l'aggiornamento.",
"general": "Generale", "general": "Generale",
"configure_smtp_to_send_emails": "Abilita le notifiche email per avvisare gli utenti quando viene rilevato un accesso da un nuovo dispositivo o posizione.", "configure_smtp_to_send_emails": "Abilita le notifiche email per avvisare gli utenti quando viene rilevato un accesso da un nuovo dispositivo o posizione.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Locale", "locale": "Locale",
"ldap_id": "ID LDAP", "ldap_id": "ID LDAP",
"reauthentication": "Riautenticazione", "reauthentication": "Riautenticazione",
"clear_filters": "Cancella filtri" "clear_filters": "Cancella filtri",
"default_profile_picture": "Immagine del profilo predefinita",
"light": "Luce",
"dark": "Buio",
"system": "Sistema"
} }

View File

@@ -120,9 +120,9 @@
"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_can_only_contain": "ユーザー名には小文字、数字、アンダー スコア、ドット、ハイフン、および '@' 記号のみを含めることができます",
"username_must_start_with": "Username must start with an alphanumeric character", "username_must_start_with": "ユーザー名は英数字で始まる必要があります",
"username_must_end_with": "Username must end with an alphanumeric character", "username_must_end_with": "ユーザー名は英数字で終わる必要があります",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "以下のコードを使用してサインインしてください。コードは15分後に失効します。", "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "以下のコードを使用してサインインしてください。コードは15分後に失効します。",
"or_visit": "or visit", "or_visit": "or visit",
"added_on": "追加日", "added_on": "追加日",
@@ -139,7 +139,7 @@
"add_api_key": "API キーを追加", "add_api_key": "API キーを追加",
"manage_api_keys": "API キーの管理", "manage_api_keys": "API キーの管理",
"api_key_created": "APIキー が作成されました", "api_key_created": "APIキー が作成されました",
"for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.", "for_security_reasons_this_key_will_only_be_shown_once": "セキュリティ上の理由から、このキーは一度だけ表示されます。安全に保管してください。",
"description": "説明", "description": "説明",
"api_key": "API キー", "api_key": "API キー",
"close": "閉じる", "close": "閉じる",
@@ -155,16 +155,16 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "APIキー \"{apiKeyName}\" を本当に削除しますかこのAPI キーを使用しているすべての連携が停止します。", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "APIキー \"{apiKeyName}\" を本当に削除しますかこのAPI キーを使用しているすべての連携が停止します。",
"last_used": "最終使用日", "last_used": "最終使用日",
"actions": "Actions", "actions": "Actions",
"images_updated_successfully": "画像が正常に更新されました", "images_updated_successfully": "画像の更新が正常に完了しました。更新には数分かかる場合があります。",
"general": "一般", "general": "一般",
"configure_smtp_to_send_emails": "新しいデバイスや場所からのログインが検出された際にユーザーに警告するメール通知を有効にします。", "configure_smtp_to_send_emails": "新しいデバイスや場所からのログインが検出された際にユーザーに警告するメール通知を有効にします。",
"ldap": "LDAP", "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.", "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP サーバーからユーザーとグループを同期するように LDAP 設定を構成します。",
"images": "画像", "images": "画像",
"update": "更新", "update": "更新",
"email_configuration_updated_successfully": "メール設定が正常に更新されました", "email_configuration_updated_successfully": "メール設定が正常に更新されました",
"save_changes_question": "変更を保存しますか?", "save_changes_question": "変更を保存しますか?",
"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?", "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": "テストメールがあなたのメールアドレスに正常に送信されました。",
"failed_to_send_test_email": "テストメールの送信に失敗しました。詳細についてはサーバーのログを確認してください。", "failed_to_send_test_email": "テストメールの送信に失敗しました。詳細についてはサーバーのログを確認してください。",
@@ -176,8 +176,8 @@
"smtp_from": "SMTP 送信元", "smtp_from": "SMTP 送信元",
"smtp_tls_option": "SMTP TLS オプション", "smtp_tls_option": "SMTP TLS オプション",
"email_tls_option": "メール TLS オプション", "email_tls_option": "メール TLS オプション",
"skip_certificate_verification": "Skip Certificate Verification", "skip_certificate_verification": "証明書検証をスキップ",
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.", "this_can_be_useful_for_selfsigned_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": "ユーザーが新しいデバイスからログインした際にメールを送信する。",
@@ -192,8 +192,8 @@
"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", "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.", "whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "ユーザのEメールをOIDCクライアントで検証済みとしてマークするかどうか。",
"ldap_configuration_updated_successfully": "LDAP 設定が正常に更新されました", "ldap_configuration_updated_successfully": "LDAP 設定が正常に更新されました",
"ldap_disabled_successfully": "LDAPは正常に無効化されました", "ldap_disabled_successfully": "LDAPは正常に無効化されました",
"ldap_sync_finished": "LDAP同期が完了しました", "ldap_sync_finished": "LDAP同期が完了しました",
@@ -208,19 +208,19 @@
"the_search_filter_to_use_to_search_or_sync_groups": "グループの検索、同期に使用する検索フィルター。", "the_search_filter_to_use_to_search_or_sync_groups": "グループの検索、同期に使用する検索フィルター。",
"attribute_mapping": "属性マッピング", "attribute_mapping": "属性マッピング",
"user_unique_identifier_attribute": "User Unique Identifier Attribute", "user_unique_identifier_attribute": "User Unique Identifier Attribute",
"the_value_of_this_attribute_should_never_change": "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_first_name_attribute": "ユーザーの名属性",
"user_last_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": "この属性の値は、URL、バイナリ、またはBase64エンコードされた画像のいずれかです。", "the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "この属性の値は、URL、バイナリ、またはBase64エンコードされた画像のいずれかです。",
"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.", "the_attribute_to_use_for_querying_members_of_a_group": "グループのメンバーをクエリする際に使用する属性。",
"group_unique_identifier_attribute": "Group Unique Identifier Attribute", "group_unique_identifier_attribute": "グループ固有識別子属性",
"group_rdn_attribute": "Group RDN Attribute (in DN)", "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.", "members_of_this_group_will_have_admin_privileges_in_pocketid": "このグループのメンバーはPocket IDで管理者権限を持っています。",
"disable": "無効", "disable": "無効",
"sync_now": "今すぐ同期", "sync_now": "今すぐ同期",
"enable": "有効", "enable": "有効",
@@ -266,7 +266,7 @@
"user_group_details_name": "ユーザーグループの詳細 {name}", "user_group_details_name": "ユーザーグループの詳細 {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": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。競合が発生した場合、ユーザーに定義されたカスタムクレームが優先されます。", "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 client created successfully", "oidc_client_created_successfully": "OIDC クライアントが正常に作成されました",
"create_oidc_client": "OIDC クライアントの作成", "create_oidc_client": "OIDC クライアントの作成",
"add_a_new_oidc_client_to_appname": "{appName} に新しい OIDC クライアントを追加します。", "add_a_new_oidc_client_to_appname": "{appName} に新しい OIDC クライアントを追加します。",
"add_oidc_client": "OIDC クライアントを追加", "add_oidc_client": "OIDC クライアントを追加",
@@ -286,8 +286,8 @@
"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?", "are_you_sure_you_want_to_delete_this_oidc_client": "このOIDCクライアントを削除してもよろしいですか",
"oidc_client_deleted_successfully": "OIDC client deleted successfully", "oidc_client_deleted_successfully": "OIDC クライアントが正常に削除されました",
"authorization_url": "Authorization URL", "authorization_url": "Authorization URL",
"oidc_discovery_url": "OIDC Discovery URL", "oidc_discovery_url": "OIDC Discovery URL",
"token_url": "Token URL", "token_url": "Token URL",
@@ -353,7 +353,7 @@
"the_device_has_been_authorized": "デバイスは認証されました。", "the_device_has_been_authorized": "デバイスは認証されました。",
"enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。", "enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。",
"authorize": "Authorize", "authorize": "Authorize",
"federated_client_credentials": "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.", "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_federated_client_credential": "Add Federated Client Credential",
"add_another_federated_client_credential": "Add another federated client credential", "add_another_federated_client_credential": "Add another federated client credential",
@@ -461,5 +461,9 @@
"locale": "ロケール", "locale": "ロケール",
"ldap_id": "LDAP ID", "ldap_id": "LDAP ID",
"reauthentication": "再認証", "reauthentication": "再認証",
"clear_filters": "フィルターをクリア" "clear_filters": "フィルターをクリア",
"default_profile_picture": "デフォルトのプロフィール画像",
"light": "光",
"dark": "闇",
"system": "システム"
} }

View File

@@ -103,8 +103,8 @@
"device": "기기", "device": "기기",
"client": "클라이언트", "client": "클라이언트",
"unknown": "알 수 없음", "unknown": "알 수 없음",
"account_details_updated_successfully": "계정 세부 사항이 성공적으로 업데이트되었습니다", "account_details_updated_successfully": "계정 정보가 성공적으로 변경되었습니다",
"profile_picture_updated_successfully": "프로필 사진이 성공적으로 업데이트되었습니다. 업데이트 적용까지 몇 분 정도 걸릴 수 있습니다.", "profile_picture_updated_successfully": "프로필 사진이 성공적으로 변경되었습니다. 변경 적용까지 몇 분 정도 걸릴 수 있습니다.",
"account_settings": "계정 설정", "account_settings": "계정 설정",
"passkey_missing": "패스키가 없습니다", "passkey_missing": "패스키가 없습니다",
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "계정 접근 권한을 잃지 않기 위해 패스키를 추가해주세요.", "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "계정 접근 권한을 잃지 않기 위해 패스키를 추가해주세요.",
@@ -131,11 +131,11 @@
"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": "{passkeyName} 삭제", "delete_passkey_name": "{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": "API 키 생성", "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_a_new_api_key_for_programmatic_access": "<link href='https://pocket-id.org/docs/api'>Pocket ID API에</link> 대한 프로그램 접근을 위해 새 API 키를 추가하세요.",
"add_api_key": "API 키 추가", "add_api_key": "API 키 추가",
"manage_api_keys": "API 키 관리", "manage_api_keys": "API 키 관리",
"api_key_created": "API 키 생성됨", "api_key_created": "API 키 생성됨",
@@ -155,14 +155,14 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "API 키 \"{apiKeyName}\"를 정말로 취소하시겠습니까? 이 키를 사용하는 모든 통합이 작동하지 않습니다.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "API 키 \"{apiKeyName}\"를 정말로 취소하시겠습니까? 이 키를 사용하는 모든 통합이 작동하지 않습니다.",
"last_used": "마지막 사용", "last_used": "마지막 사용",
"actions": "동작", "actions": "동작",
"images_updated_successfully": "이미지가 성공적으로 업데이트되었습니다", "images_updated_successfully": "이미지가 성공적으로 변경되었습니다. 변경에는 몇 분 정도 소요될 수 있습니다.",
"general": "일반", "general": "일반",
"configure_smtp_to_send_emails": "새로운 기기나 위치에서 로그인 감지 시 이메일 알림을 활성화합니다.", "configure_smtp_to_send_emails": "새로운 기기나 위치에서 로그인 감지 시 이메일 알림을 활성화합니다.",
"ldap": "LDAP", "ldap": "LDAP",
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP 서버에서 사용자와 그룹을 동기화하기 위해 LDAP을 구성합니다.", "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP 서버에서 사용자와 그룹을 동기화하기 위해 LDAP을 구성합니다.",
"images": "이미지", "images": "이미지",
"update": "업데이트", "update": "업데이트",
"email_configuration_updated_successfully": "이메일 설정이 성공적으로 업데이트되었습니다", "email_configuration_updated_successfully": "이메일 구성이 성공적으로 변경되었습니다",
"save_changes_question": "변경 내용을 저장하시겠습니까?", "save_changes_question": "변경 내용을 저장하시겠습니까?",
"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": "저장하고 전송하기",
@@ -186,7 +186,7 @@
"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": "관리자가 사용자에게 로그인 코드를 전송할 수 있게 합니다.",
"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": "사용자가 다시 로그인하기 전 세션의 시간(분)입니다.",
@@ -194,7 +194,7 @@
"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": "OIDC 클라이언트에게 사용자의 이메일이 인증된 것으로 표시합니다.", "whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "OIDC 클라이언트에게 사용자의 이메일이 인증된 것으로 표시합니다.",
"ldap_configuration_updated_successfully": "LDAP 구성이 성공적으로 업데이트되었습니다", "ldap_configuration_updated_successfully": "LDAP 구성이 성공적으로 변경되었습니다",
"ldap_disabled_successfully": "LDAP가 성공적으로 비활성화되었습니다", "ldap_disabled_successfully": "LDAP가 성공적으로 비활성화되었습니다",
"ldap_sync_finished": "LDAP 동기화 완료", "ldap_sync_finished": "LDAP 동기화 완료",
"client_configuration": "클라이언트 구성", "client_configuration": "클라이언트 구성",
@@ -301,7 +301,7 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "새로운 클라이언트 시크릿 생성하시겠습니까? 기존 클라이언트 시크릿은 무효화됩니다.", "are_you_sure_you_want_to_create_a_new_client_secret": "새로운 클라이언트 시크릿 생성하시겠습니까? 기존 클라이언트 시크릿은 무효화됩니다.",
"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 클라이언트 {name}", "oidc_client_name": "OIDC 클라이언트 {name}",
"client_id": "클라이언트 ID", "client_id": "클라이언트 ID",
"client_secret": "클라이언트 시크릿", "client_secret": "클라이언트 시크릿",
@@ -395,7 +395,7 @@
"configure_user_creation": "사용자 생성 설정을 관리합니다. 여기에는 신규 사용자 등록 방법 및 신규 사용자의 기본 권한이 포함됩니다.", "configure_user_creation": "사용자 생성 설정을 관리합니다. 여기에는 신규 사용자 등록 방법 및 신규 사용자의 기본 권한이 포함됩니다.",
"user_creation_groups_description": "새 사용자가 계정을 만들 때 이 그룹을 자동으로 할당합니다.", "user_creation_groups_description": "새 사용자가 계정을 만들 때 이 그룹을 자동으로 할당합니다.",
"user_creation_claims_description": "새 사용자가 계정을 만들 때 이 사용자 정의 클레임을 자동으로 할당합니다.", "user_creation_claims_description": "새 사용자가 계정을 만들 때 이 사용자 정의 클레임을 자동으로 할당합니다.",
"user_creation_updated_successfully": "사용자 생성 설정이 성공적으로 업데이트되었습니다.", "user_creation_updated_successfully": "사용자 생성 설정이 성공적으로 변경되었습니다.",
"signup_disabled_description": "계정 생성이 완전히 비활성화됩니다. 새로운 사용자 계정은 관리자만 생성할 수 있습니다.", "signup_disabled_description": "계정 생성이 완전히 비활성화됩니다. 새로운 사용자 계정은 관리자만 생성할 수 있습니다.",
"signup_requires_valid_token": "계정을 생성하려면 유효한 가입 토큰이 필요합니다", "signup_requires_valid_token": "계정을 생성하려면 유효한 가입 토큰이 필요합니다",
"validating_signup_token": "가입 토큰 검증", "validating_signup_token": "가입 토큰 검증",
@@ -461,5 +461,9 @@
"locale": "로케일", "locale": "로케일",
"ldap_id": "LDAP ID", "ldap_id": "LDAP ID",
"reauthentication": "재인증", "reauthentication": "재인증",
"clear_filters": "필터 지우기" "clear_filters": "필터 지우기",
"default_profile_picture": "기본 프로필 사진",
"light": "빛",
"dark": "어둠",
"system": "시스템"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet je zeker dat je de API-sleutel \"{apiKeyName}\" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet je zeker dat je de API-sleutel \"{apiKeyName}\" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.",
"last_used": "Laatst gebruikt", "last_used": "Laatst gebruikt",
"actions": "Acties", "actions": "Acties",
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt", "images_updated_successfully": "Afbeeldingen zijn bijgewerkt. Het kan even duren voordat alles is bijgewerkt.",
"general": "Algemeen", "general": "Algemeen",
"configure_smtp_to_send_emails": "Zet e-mailmeldingen aan om gebruikers te laten weten als iemand inlogt vanaf een nieuw apparaat of een nieuwe plek.", "configure_smtp_to_send_emails": "Zet e-mailmeldingen aan om gebruikers te laten weten als iemand inlogt vanaf een nieuw apparaat of een nieuwe plek.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Locatie", "locale": "Locatie",
"ldap_id": "LDAP-ID", "ldap_id": "LDAP-ID",
"reauthentication": "Opnieuw inloggen", "reauthentication": "Opnieuw inloggen",
"clear_filters": "Filters wissen" "clear_filters": "Filters wissen",
"default_profile_picture": "Standaard profielfoto",
"light": "Licht",
"dark": "Donker",
"system": "Systeem"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Czy na pewno chcesz unieważnić klucz API \"{apiKeyName}\"? To przerwie wszelkie integracje korzystające z tego klucza.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Czy na pewno chcesz unieważnić klucz API \"{apiKeyName}\"? To przerwie wszelkie integracje korzystające z tego klucza.",
"last_used": "Ostatnio używane", "last_used": "Ostatnio używane",
"actions": "Akcje", "actions": "Akcje",
"images_updated_successfully": "Sukces! Obrazy zostały zaktualizowane.", "images_updated_successfully": "Obrazy zostały pomyślnie zaktualizowane. Aktualizacja może potrwać kilka minut.",
"general": "Ogólne", "general": "Ogólne",
"configure_smtp_to_send_emails": "Włącz powiadomienia e-mail, aby informować użytkowników, gdy logowanie zostanie wykryte z nowego urządzenia lub lokalizacji.", "configure_smtp_to_send_emails": "Włącz powiadomienia e-mail, aby informować użytkowników, gdy logowanie zostanie wykryte z nowego urządzenia lub lokalizacji.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Ustawienia regionalne", "locale": "Ustawienia regionalne",
"ldap_id": "Identyfikator LDAP", "ldap_id": "Identyfikator LDAP",
"reauthentication": "Ponowne uwierzytelnianie", "reauthentication": "Ponowne uwierzytelnianie",
"clear_filters": "Wyczyść filtry" "clear_filters": "Wyczyść filtry",
"default_profile_picture": "Domyślne zdjęcie profilowe",
"light": "Światło",
"dark": "Ciemny",
"system": "System"
} }

View File

@@ -16,7 +16,7 @@
"click_profile_picture_to_upload_custom": "Clique na foto de perfil para enviar uma imagem personalizada dos seus arquivos.", "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.", "image_should_be_in_format": "A imagem deve estar no formato PNG ou JPEG.",
"items_per_page": "Itens por página", "items_per_page": "Itens por página",
"no_items_found": "Nenhum item encontrado", "no_items_found": "Nada foi encontrado",
"select_items": "Selecione os itens...", "select_items": "Selecione os itens...",
"search": "Pesquisar...", "search": "Pesquisar...",
"expand_card": "Expandir cartão", "expand_card": "Expandir cartão",
@@ -27,7 +27,7 @@
"alternative_sign_in_methods": "Outras formas de entrar", "alternative_sign_in_methods": "Outras formas de entrar",
"login_background": "Histórico de login", "login_background": "Histórico de login",
"logo": "Logotipo", "logo": "Logotipo",
"login_code": "Código de Login:", "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.", "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", "one_hour": "1 hora",
"twelve_hours": "12 horas", "twelve_hours": "12 horas",
@@ -38,14 +38,14 @@
"generate_code": "Gerar Código", "generate_code": "Gerar Código",
"name": "Nome", "name": "Nome",
"browser_unsupported": "Navegador não suportado", "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", "an_unknown_error_occurred": "Ocorreu um erro desconhecido",
"authentication_process_was_aborted": "O processo de autenticação foi abortado", "authentication_process_was_aborted": "O processo de autenticação foi abortado",
"error_occurred_with_authenticator": "Ocorreu um erro com o autenticador", "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_discoverable_credentials": "O autenticador não suporta credenciais detectáveis",
"authenticator_does_not_support_resident_keys": "O autenticador não aceita chaves residentes.", "authenticator_does_not_support_resident_keys": "O autenticador não aceita chaves residentes",
"passkey_was_previously_registered": "Esta chave de acesso já está registrada", "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_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", "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.", "critical_error_occurred_contact_administrator": "Ocorreu um erro grave. Por favor, entre em contato com o administrador.",
"sign_in_to": "Entrar em {name}", "sign_in_to": "Entrar em {name}",
@@ -68,7 +68,7 @@
"please_try_to_sign_in_again": "Tente entrar novamente.", "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_with_passkey_to_access_account": "Identifique-se com sua chave de acesso para entrar em sua conta.",
"authenticate": "Autenticar", "authenticate": "Autenticar",
"please_try_again": "Por favor tente novamente.", "please_try_again": "Por favor, tente novamente.",
"continue": "Continuar", "continue": "Continuar",
"alternative_sign_in": "Entrar de outra forma", "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 perdeu 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.",
@@ -76,7 +76,7 @@
"email_login": "Entrar com e-mail", "email_login": "Entrar com e-mail",
"enter_a_login_code_to_sign_in": "Digite um código de login para entrar.", "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", "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", "go_back": "Voltar",
"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.", "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_code": "Digite o código",
@@ -120,9 +120,9 @@
"last_name": "Sobrenome", "last_name": "Sobrenome",
"username": "Nome de usuário", "username": "Nome de usuário",
"save": "Salvar", "save": "Salvar",
"username_can_only_contain": "O nome de usuário só pode conter letras minúsculas, números, subtraço, pontos, hífens e símbolos '@'", "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_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_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.", "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", "or_visit": "ou visite",
"added_on": "Adicionado em", "added_on": "Adicionado em",
@@ -145,9 +145,9 @@
"close": "Fechar", "close": "Fechar",
"name_to_identify_this_api_key": "Nome para identificar esta chave API.", "name_to_identify_this_api_key": "Nome para identificar esta chave API.",
"expires_at": "Valido até", "expires_at": "Valido até",
"when_this_api_key_will_expire": "Quando esta chave API vai expirar.", "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.", "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", "revoke_api_key": "Revogar chave API",
"never": "Nunca", "never": "Nunca",
"revoke": "Revogar", "revoke": "Revogar",
@@ -155,7 +155,7 @@
"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.", "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", "last_used": "Último uso",
"actions": "Ações", "actions": "Ações",
"images_updated_successfully": "Imagens atualizadas com sucesso", "images_updated_successfully": "Imagens atualizadas com sucesso. A atualização pode demorar alguns minutos.",
"general": "Geral", "general": "Geral",
"configure_smtp_to_send_emails": "Ative as notificações por e-mail para avisar os usuários quando um login for detectado em um novo dispositivo ou local.", "configure_smtp_to_send_emails": "Ative as notificações por e-mail para avisar os usuários quando um login for detectado em um novo dispositivo ou local.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -186,7 +186,7 @@
"email_login_code_from_admin": "Código de login por e-mail do administrador", "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 para o e-mail do usuário.", "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", "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", "application_name": "Nome do aplicativo",
"session_duration": "Duração da sessão", "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.", "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.",
@@ -208,19 +208,19 @@
"the_search_filter_to_use_to_search_or_sync_groups": "O filtro de pesquisa utilizado para encontrar/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", "attribute_mapping": "Mapeamento de atributos",
"user_unique_identifier_attribute": "Atributo de identificador único do usuário", "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", "username_attribute": "Atributo do nome de usuário",
"user_mail_attribute": "Atributo de e-mail do usuário", "user_mail_attribute": "Atributo de e-mail do usuário",
"user_first_name_attribute": "Atributo do primeiro nome 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_last_name_attribute": "Atributo do sobrenome do usuário",
"user_profile_picture_attribute": "Atributo da foto do perfil 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", "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.", "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_unique_identifier_attribute": "Atributo identificador exclusivo do grupo",
"group_rdn_attribute": "Atributo RDN do grupo (em DN)", "group_rdn_attribute": "Atributo RDN do grupo (em DN)",
"admin_group_name": "Nome do grupo de administradores", "admin_group_name": "Nome do grupo de administradores",
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Os membros desse grupo terão 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", "disable": "Desativar",
"sync_now": "Sincronizar agora", "sync_now": "Sincronizar agora",
"enable": "Ativar", "enable": "Ativar",
@@ -251,7 +251,7 @@
"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.", "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", "user_group_created_successfully": "Grupo de usuários criado com sucesso",
"create_user_group": "Criar Grupo de Usuários", "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", "add_group": "Adicionar Grupo",
"manage_user_groups": "Gerenciar grupos de usuários", "manage_user_groups": "Gerenciar grupos de usuários",
"friendly_name": "Nome Amigável", "friendly_name": "Nome Amigável",
@@ -295,10 +295,10 @@
"logout_url": "URL de logout", "logout_url": "URL de logout",
"certificate_url": "URL do certificado", "certificate_url": "URL do certificado",
"enabled": "Habilitado", "enabled": "Habilitado",
"disabled": "Inativo", "disabled": "Desativado",
"oidc_client_updated_successfully": "Cliente OIDC atualizado com sucesso", "oidc_client_updated_successfully": "Cliente OIDC atualizado com sucesso",
"create_new_client_secret": "Criar novo segredo do cliente", "create_new_client_secret": "Criar novo segredo do cliente",
"are_you_sure_you_want_to_create_a_new_client_secret": "Tem certeza que deseja criar um novo segredo de cliente? O antigo 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", "generate": "Gerar",
"new_client_secret_created_successfully": "Novo segredo do cliente criado com sucesso", "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", "allowed_user_groups_updated_successfully": "Grupos de usuários permitidos atualizados com sucesso",
@@ -394,9 +394,9 @@
"user_creation": "Criar usuário", "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.", "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_groups_description": "Atribuir esses grupos automaticamente aos novos usuários no momento da inscrição.",
"user_creation_claims_description": "Atribua essas reivindicações personalizadas 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.", "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_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", "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", "validating_signup_token": "Validando o token de inscrição",
"go_to_login": "Vá para o login", "go_to_login": "Vá para o login",
@@ -409,7 +409,7 @@
"account_created": "Conta criada", "account_created": "Conta criada",
"enable_user_signups": "Ativar inscrições de usuários", "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.", "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", "create_signup_token": "Criar token de inscrição",
"view_active_signup_tokens": "Ver tokens de inscrição ativos", "view_active_signup_tokens": "Ver tokens de inscrição ativos",
"manage_signup_tokens": "Gerenciar tokens de inscrição", "manage_signup_tokens": "Gerenciar tokens de inscrição",
@@ -451,7 +451,7 @@
"display_name": "Nome de exibição", "display_name": "Nome de exibição",
"configure_application_images": "Configurar imagens de aplicativos", "configure_application_images": "Configurar imagens de aplicativos",
"ui_config_disabled_info_title": "Configuração da interface do usuário desativada", "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>.", "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", "invalid_url": "URL inválido",
"require_user_email": "É preciso um endereço de e-mail", "require_user_email": "É preciso um endereço de e-mail",
@@ -461,5 +461,9 @@
"locale": "Localização", "locale": "Localização",
"ldap_id": "ID LDAP", "ldap_id": "ID LDAP",
"reauthentication": "Re-autenticação", "reauthentication": "Re-autenticação",
"clear_filters": "Limpar filtros" "clear_filters": "Limpar filtros",
"default_profile_picture": "Foto de perfil padrão",
"light": "Luz",
"dark": "Escuro",
"system": "Sistema"
} }

View File

@@ -6,17 +6,17 @@
"docs": "Документация", "docs": "Документация",
"key": "Ключ", "key": "Ключ",
"value": "Значение", "value": "Значение",
"remove_custom_claim": "Удалить пользовательский claim", "remove_custom_claim": "Удалить пользовательское утверждение",
"add_custom_claim": "Добавить пользовательский 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": "Изображение профиля управляется LDAP сервером и не может быть изменено здесь.", "profile_picture_is_managed_by_ldap_server": "Изображение профиля управляется сервером LDAP и не может быть изменено здесь.",
"click_profile_picture_to_upload_custom": "Нажмите на изображение профиля, чтобы загрузить его из ваших файлов.", "click_profile_picture_to_upload_custom": "Нажмите на изображение профиля, чтобы загрузить его из ваших файлов.",
"image_should_be_in_format": "Изображение должно быть в формате PNG или JPEG.", "image_should_be_in_format": "Изображение должно быть в формате PNG или JPEG.",
"items_per_page": "Элементов на странице", "items_per_page": "Элементов на странице",
"no_items_found": "Элементов не найдено", "no_items_found": "Элементы не найдены",
"select_items": "Выбрать элементы...", "select_items": "Выбрать элементы...",
"search": "Поиск...", "search": "Поиск...",
"expand_card": "Развернуть карточку", "expand_card": "Развернуть карточку",
@@ -41,21 +41,21 @@
"this_browser_does_not_support_passkeys": "Этот браузер не поддерживает пасскеи. Пожалуйста, воспользуйтесь альтернативным способом входа.", "this_browser_does_not_support_passkeys": "Этот браузер не поддерживает пасскеи. Пожалуйста, воспользуйтесь альтернативным способом входа.",
"an_unknown_error_occurred": "Произошла неизвестная ошибка", "an_unknown_error_occurred": "Произошла неизвестная ошибка",
"authentication_process_was_aborted": "Процесс аутентификации был прерван", "authentication_process_was_aborted": "Процесс аутентификации был прерван",
"error_occurred_with_authenticator": "С аутентификатором произошла ошибка", "error_occurred_with_authenticator": "Произошла ошибка аутентификатора",
"authenticator_does_not_support_discoverable_credentials": "Аутентификатор не поддерживает discoverable credentials", "authenticator_does_not_support_discoverable_credentials": "Аутентификатор не поддерживает обнаруживаемые учетные данные",
"authenticator_does_not_support_resident_keys": "Аутентификатор не поддерживает resident keys", "authenticator_does_not_support_resident_keys": "Аутентификатор не поддерживает резидентные ключи",
"passkey_was_previously_registered": "Этот пасскей был ранее зарегистрирован", "passkey_was_previously_registered": "Этот пасскей был ранее зарегистрирован",
"authenticator_does_not_support_any_of_the_requested_algorithms": "Аутентификатор не поддерживает ни один из запрошенных алгоритмов", "authenticator_does_not_support_any_of_the_requested_algorithms": "Аутентификатор не поддерживает ни один из запрошенных алгоритмов",
"authenticator_timed_out": "Время ожидания аутентификатора истекло", "authenticator_timed_out": "Время ожидания аутентификатора истекло",
"critical_error_occurred_contact_administrator": "Произошла критическая ошибка. Обратитесь к администратору.", "critical_error_occurred_contact_administrator": "Произошла критическая ошибка. Обратитесь к администратору.",
"sign_in_to": "Вход в {name}", "sign_in_to": "Войти в {name}",
"client_not_found": "Клиент не найден", "client_not_found": "Клиент не найден",
"client_wants_to_access_the_following_information": "<b>{client}</b> запрашивает доступ к следующей информации:", "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}?", "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Вы хотите войти в <b>{client}</b> с помощью вашей учетной записи {appName}?",
"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": "Отменить",
@@ -64,7 +64,7 @@
"client_logo": "Логотип клиента", "client_logo": "Логотип клиента",
"sign_out": "Выйти", "sign_out": "Выйти",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Вы хотите выйти из {appName} с учетной записью <b>{username}</b>?", "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": "Пожалуйста, попробуйте войти снова.", "please_try_to_sign_in_again": "Пожалуйста, попробуйте войти снова.",
"authenticate_with_passkey_to_access_account": "Авторизуйтесь с использованием пасскея для доступа к вашей учетной записи.", "authenticate_with_passkey_to_access_account": "Авторизуйтесь с использованием пасскея для доступа к вашей учетной записи.",
"authenticate": "Авторизоваться", "authenticate": "Авторизоваться",
@@ -74,8 +74,8 @@
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Если у вас нет доступа к вашему пасскею, вы можете войти одним из следующих способов.", "if_you_do_not_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": "Письмо было отправлено на указанный адрес электронной почты, если он существует в системе.",
@@ -83,23 +83,23 @@
"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": "Неправильный URL-адрес перенаправления", "invalid_redirect_url": "Неправильный URL-адрес перенаправления",
"audit_log": "Журнал аудита", "audit_log": "Журнал аудита",
"users": "Пользователи", "users": "Пользователи",
"user_groups": "Группы пользователей", "user_groups": "Группы пользователей",
"oidc_clients": "Клиенты OIDC", "oidc_clients": "Клиенты OIDC",
"api_keys": "API ключи", "api_keys": "Ключи API",
"application_configuration": "Конфигурация приложения", "application_configuration": "Конфигурация приложения",
"settings": "Настройки", "settings": "Настройки",
"update_pocket_id": "Обновите Pocket ID", "update_pocket_id": "Обновите Pocket ID",
"powered_by": "Powered by", "powered_by": "Работает на",
"see_your_account_activities_from_the_last_3_months": "Смотрите активность вашей учетной записи за последние 3 месяца.", "see_your_account_activities_from_the_last_3_months": "Смотрите активность своей учетной записи за последние 3 месяца.",
"time": "Время", "time": "Время",
"event": "Событие", "event": "Событие",
"approximate_location": "Приблизительное местоположение", "approximate_location": "Примерное местоположение",
"ip_address": "IP адрес", "ip_address": "IP-адрес",
"device": "Устройство", "device": "Устройство",
"client": "Клиент", "client": "Клиент",
"unknown": "Неизвестно", "unknown": "Неизвестно",
@@ -120,7 +120,7 @@
"last_name": "Фамилия", "last_name": "Фамилия",
"username": "Имя пользователя", "username": "Имя пользователя",
"save": "Сохранить", "save": "Сохранить",
"username_can_only_contain": "Имя пользователя может содержать только строчные буквы, цифры, знак подчеркивания, точки, дефиса и символ '@'", "username_can_only_contain": "Имя пользователя может содержать только строчные буквы, цифры, подчеркивания, точки, дефисы и символ '@'",
"username_must_start_with": "Имя пользователя должно начинаться с буквы или цифры", "username_must_start_with": "Имя пользователя должно начинаться с буквы или цифры",
"username_must_end_with": "Имя пользователя должно заканчиваться буквой или цифрой", "username_must_end_with": "Имя пользователя должно заканчиваться буквой или цифрой",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Войдите, используя следующий код. Код истечет через 15 минут.", "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Войдите, используя следующий код. Код истечет через 15 минут.",
@@ -134,29 +134,29 @@
"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": "Создать 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_a_new_api_key_for_programmatic_access": "Добавить новый ключ API для программного доступа к <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>.",
"add_api_key": "Добавить API ключ", "add_api_key": "Добавить ключ API",
"manage_api_keys": "Управление API ключами", "manage_api_keys": "Управление ключами API",
"api_key_created": "API ключ создан", "api_key_created": "Ключ API создан",
"for_security_reasons_this_key_will_only_be_shown_once": "По соображениям безопасности, этот ключ будет показан только один раз. Пожалуйста, храните его в безопасном месте.", "for_security_reasons_this_key_will_only_be_shown_once": "По соображениям безопасности, этот ключ будет показан только один раз. Пожалуйста, храните его в безопасном месте.",
"description": "Описание", "description": "Описание",
"api_key": "API ключ", "api_key": "Ключ API",
"close": "Закрыть", "close": "Закрыть",
"name_to_identify_this_api_key": "Имя для идентификации API ключа.", "name_to_identify_this_api_key": "Имя для идентификации ключа API.",
"expires_at": "Действителен до", "expires_at": "Действителен до",
"when_this_api_key_will_expire": "Когда срок действия этого API ключа истечет.", "when_this_api_key_will_expire": "Когда истечет срок действия этого ключа API.",
"optional_description_to_help_identify_this_keys_purpose": "Опциональное описание, чтобы помочь определить цель этого ключа.", "optional_description_to_help_identify_this_keys_purpose": "Необязательное описание, которое поможет определить назначение этого ключа.",
"expiration_date_must_be_in_the_future": "Дата истечения должна быть определена в будущем", "expiration_date_must_be_in_the_future": "Дата истечения срока действия должна быть в будущем",
"revoke_api_key": "Отозвать API ключ", "revoke_api_key": "Отозвать ключ API",
"never": "Никогда", "never": "Никогда",
"revoke": "Отозвать", "revoke": "Отозвать",
"api_key_revoked_successfully": "API ключ успешно отозван", "api_key_revoked_successfully": "Ключ API успешно отозван",
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Вы уверены, что хотите отозвать API ключ \"{apiKeyName}\"? Это разрушит интеграцию, использующую этот ключ.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Вы уверены, что хотите отозвать ключ API \"{apiKeyName}\"? Любые интеграции, использующие этот ключ, перестанут работать.",
"last_used": "Последнее использование", "last_used": "Последнее использование",
"actions": "Действия", "actions": "Действия",
"images_updated_successfully": "Изображения успешно обновлены", "images_updated_successfully": "Изображения обновились, но может занять пару минут.",
"general": "Основное", "general": "Общее",
"configure_smtp_to_send_emails": "Включить уведомления пользователей по электронной почте при обнаружении логина с нового устройства или локации.", "configure_smtp_to_send_emails": "Включить уведомления пользователей по электронной почте при обнаружении логина с нового устройства или локации.",
"ldap": "LDAP", "ldap": "LDAP",
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.", "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.",
@@ -274,10 +274,10 @@
"one_time_link": "Одноразовая ссылка", "one_time_link": "Одноразовая ссылка",
"use_this_link_to_sign_in_once": "Используйте эту ссылку, чтобы войти единожды. Это необходимо для пользователей, которые ещё не добавили пасскей или потеряли его.", "use_this_link_to_sign_in_once": "Используйте эту ссылку, чтобы войти единожды. Это необходимо для пользователей, которые ещё не добавили пасскей или потеряли его.",
"add": "Добавить", "add": "Добавить",
"callback_urls": "Callback URLs", "callback_urls": "URL-адреса обратного вызова",
"logout_callback_urls": "Logout Callback URLs", "logout_callback_urls": "URL-адреса обратного вызова при выходе",
"public_client": "Публичный клиент", "public_client": "Публичный клиент",
"public_clients_description": "Публичные клиенты не имеют клиентского секрета. Они предназначены для мобильных, SPA и нативных приложений, где секретные данные нельзя надежно хранить.", "public_clients_description": "Публичные клиенты не имеют секрета клиента. Они предназначены для мобильных, SPA и нативных приложений, где секреты нельзя надежно хранить.",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
"requires_reauthentication": "Требуется повторная аутентификация", "requires_reauthentication": "Требуется повторная аутентификация",
@@ -286,31 +286,31 @@
"change_logo": "Изменить логотип", "change_logo": "Изменить логотип",
"upload_logo": "Загрузить логотип", "upload_logo": "Загрузить логотип",
"remove_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 успешно удален", "oidc_client_deleted_successfully": "Клиент OIDC успешно удален",
"authorization_url": "Authorization URL", "authorization_url": "URL-адрес авторизации",
"oidc_discovery_url": "OIDC Discovery URL", "oidc_discovery_url": "URL-адрес обнаружения OIDC",
"token_url": "Token URL", "token_url": "URL-адрес токена",
"userinfo_url": "Userinfo URL", "userinfo_url": "URL-адрес информации о пользователе",
"logout_url": "Logout URL", "logout_url": "URL-адрес выхода",
"certificate_url": "Certificate URL", "certificate_url": "URL-адрес сертификата",
"enabled": "Включен", "enabled": "Включено",
"disabled": "Отключено", "disabled": "Отключено",
"oidc_client_updated_successfully": "OIDC клиент успешно обновлен", "oidc_client_updated_successfully": "Клиент OIDC успешно обновлен",
"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": "Вы уверены, что хотите создать новый секрет клиента? Старый будет аннулирован.",
"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 клиент {name}", "oidc_client_name": "Клиент OIDC {name}",
"client_id": "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": "Добавить группы пользователей к этому клиенту для ограничения доступа пользователей в этих группах. Если группы пользователей не выбраны, все пользователи будут иметь доступ к этому клиенту.",
"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_question": "Сбросить изображение профиля?",
@@ -318,63 +318,63 @@
"reset": "Сбросить", "reset": "Сбросить",
"reset_to_default": "Сбросить по умолчанию", "reset_to_default": "Сбросить по умолчанию",
"profile_picture_has_been_reset": "Изображение профиля было сброшено. Обновление может занять несколько минут.", "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>.", "contribute_to_translation": "Если вы нашли ошибку, приглашаем вас помочь с переводом на <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": "Смотрите всю активность пользователей за последние 3 месяца.", "see_all_account_activities_from_the_last_3_months": "Смотрите активность всех пользователей за последние 3 месяца.",
"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_ui_animations": "Отключить все анимации в интерфейсе.",
"user_disabled": "Учетная запись отключена", "user_disabled": "Учетная запись отключена",
"disabled_users_cannot_log_in_or_use_services": "Отключенные пользователи не могут войти или использовать сервисы.", "disabled_users_cannot_log_in_or_use_services": "Отключенные пользователи не могут входить или использовать сервисы.",
"user_disabled_successfully": "Пользователь успешно отключен.", "user_disabled_successfully": "Пользователь успешно отключен.",
"user_enabled_successfully": "Пользователь успешно включен.", "user_enabled_successfully": "Пользователь успешно включен.",
"status": "Статус", "status": "Статус",
"disable_firstname_lastname": "Отключить {firstName} {lastName}", "disable_firstname_lastname": "Отключить {firstName} {lastName}",
"are_you_sure_you_want_to_disable_this_user": "Вы уверены, что хотите отключить этого пользователя? Они не смогут войти в систему или получить доступ к любым сервисам.", "are_you_sure_you_want_to_disable_this_user": "Вы уверены, что хотите отключить этого пользователя? Он не сможет войти или получить доступ к каким-либо сервисам.",
"ldap_soft_delete_users": "Оставить отключенных пользователей от LDAP.", "ldap_soft_delete_users": "Сохранять отключенных пользователей из LDAP.",
"ldap_soft_delete_users_description": "Когда включено, пользователи удалённые из LDAP будут отключены вместо удаления из системы.", "ldap_soft_delete_users_description": "Если включено, пользователи, удаленные из LDAP, будут отключены, а не удалены из системы.",
"login_code_email_success": "Код входа был отправлен пользователю.", "login_code_email_success": "Код входа был отправлен пользователю.",
"send_email": "Отправить письмо", "send_email": "Отправить письмо",
"show_code": "Показать код", "show_code": "Показать код",
"callback_url_description": "URL-адрес(а) предоставленные вашим клиентом. Будет автоматически добавлен если оставить пустым. Маски (*) поддерживаются, но лучше избегайте их для повышения безопасности.", "callback_url_description": "URL-адреса, предоставленные клиентом. Будет автоматически добавлен, если оставить пустым. Подстановочные знаки (*) поддерживаются, но их лучше избегать для большей безопасности.",
"logout_callback_url_description": "URL-адрес(а), предоставленный вашим клиентом для выхода. Маски (*) поддерживаются, но лучше избегайте их для повышения безопасности.", "logout_callback_url_description": "URL-адреса, предоставленные клиентом. Подстановочные знаки (*) поддерживаются, но их лучше избегать для большей безопасности.",
"api_key_expiration": "Истечение срока действия API ключа", "api_key_expiration": "Истечение срока действия ключа API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда истечет срок действия API ключа.", "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда срок действия его ключа API истекает.",
"authorize_device": "Авторизовать устройство", "authorize_device": "Авторизовать устройство",
"the_device_has_been_authorized": "Устройство авторизовано.", "the_device_has_been_authorized": "Устройство авторизовано.",
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.", "enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
"authorize": "Авторизовать", "authorize": "Авторизовать",
"federated_client_credentials": "Федеративные учетные данные клиента", "federated_client_credentials": "Федеративные учетные данные клиента",
"federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете авторизовывать OIDC клиентов, используя JWT токены, выпущенные третьими сторонами.", "federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете аутентифицировать клиентов OIDC с помощью токенов JWT, выпущенных сторонними поставщиками удостоверений.",
"add_federated_client_credential": "Добавить федеративные учетные данные клиента", "add_federated_client_credential": "Добавить федеративные учетные данные клиента",
"add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента", "add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента",
"oidc_allowed_group_count": "Кол-во разрешенных групп", "oidc_allowed_group_count": "Число разрешенных групп",
"unrestricted": "Не ограничено", "unrestricted": "Неограниченно",
"show_advanced_options": "Показать дополнительные опции", "show_advanced_options": "Показать дополнительные параметры",
"hide_advanced_options": "Скрыть дополнительные опции", "hide_advanced_options": "Скрыть дополнительные параметры",
"oidc_data_preview": "Предпросмотр данных OIDC", "oidc_data_preview": "Предварительный просмотр данных OIDC",
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Предпросмотр данных OIDC, которые будут отправлены разным пользователям", "preview_the_oidc_data_that_would_be_sent_for_different_users": "Предварительный просмотр данных OIDC, которые будут отправлены для различных пользователей",
"id_token": "ID Token", "id_token": "Токен идентификации",
"access_token": "Access Token", "access_token": "Токен доступа",
"userinfo": "Userinfo", "userinfo": "Информация о пользователе",
"id_token_payload": "Содержимое ID Token", "id_token_payload": "Полезная нагрузка токена идентификации",
"access_token_payload": "Содержимое Access Token", "access_token_payload": "Полезная нагрузка токена доступа",
"userinfo_endpoint_response": "Ответ Userinfo эндпоинта", "userinfo_endpoint_response": "Ответ конечной точки информации о пользователе",
"copy": "Копировать", "copy": "Копировать",
"no_preview_data_available": "Предварительный просмотр данных не доступен", "no_preview_data_available": "Нет данных предварительного просмотра",
"copy_all": "Копировать все", "copy_all": "Копировать все",
"preview": "Предпросмотр", "preview": "Предварительный просмотр",
"preview_for_user": "Предпросмотр для {name}", "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": "Показать", "show": "Показать",
"select_an_option": "Выберите опцию", "select_an_option": "Выберите опцию",
"select_user": "Выбрать пользователя", "select_user": "Выбрать пользователя",
@@ -442,24 +442,28 @@
"revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.", "revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.",
"revoke_access_successful": "Доступ к {clientName} успешно отозван.", "revoke_access_successful": "Доступ к {clientName} успешно отозван.",
"last_signed_in_ago": "Последний вход {time} назад", "last_signed_in_ago": "Последний вход {time} назад",
"invalid_client_id": "ID клиента может содержать только буквы, цифры, подчеркивания и дефисы", "invalid_client_id": "Идентификатор клиента может содержать только буквы, цифры, подчеркивания и дефисы",
"custom_client_id_description": "Установите пользовательский ID клиента, если это нужно для вашего приложения. Если нет, оставьте поле пустым, чтобы он был сгенерирован случайным образом.", "custom_client_id_description": "Укажите собственный идентификатор клиента, если это требуется для вашего приложения. В противном случае оставьте поле пустым, чтобы сгенерировать случайный идентификатор.",
"generated": "Сгенерированный", "generated": "Сгенерированный",
"administration": "Администрирование", "administration": "Администрирование",
"group_rdn_attribute_description": "Атрибут, который используется в различающемся имени группы (DN).", "group_rdn_attribute_description": "Атрибут, который используется в отличительном имени (DN) группы.",
"display_name_attribute": "Атрибут отображаемого имени", "display_name_attribute": "Атрибут отображаемого имени",
"display_name": "Отображаемое имя", "display_name": "Отображаемое имя",
"configure_application_images": "Настройка изображений приложения", "configure_application_images": "Настройка изображений приложения",
"ui_config_disabled_info_title": "Конфигурация пользовательского интерфейса отключена", "ui_config_disabled_info_title": "Конфигурация пользовательского интерфейса отключена",
"ui_config_disabled_info_description": "Конфигурация пользовательского интерфейса отключена, потому что настройки приложения управляются через переменные среды. Некоторые настройки могут быть недоступны для редактирования.", "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>.", "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", "invalid_url": "Неверный URL-адрес",
"require_user_email": "Требовать адрес электронной почты", "require_user_email": "Обязательный адрес электронной почты",
"require_user_email_description": "Требует, чтобы у пользователей был адрес электронной почты. Если эта функция отключена, пользователи без адреса электронной почты не смогут пользоваться функциями, для которых нужен адрес электронной почты.", "require_user_email_description": "Требует наличия адреса электронной почты у пользователей. Если отключено, пользователи без адреса электронной почты не смогут использовать функции, требующие адреса электронной почты.",
"view": "Посмотреть", "view": "Вид",
"toggle_columns": ереключать столбцы", "toggle_columns": оказывать столбцы",
"locale": "Локаль", "locale": "Язык",
"ldap_id": "LDAP-идентификатор", "ldap_id": "LDAP ID",
"reauthentication": "Повторная аутентификация", "reauthentication": "Повторная аутентификация",
"clear_filters": "Очистить фильтры" "clear_filters": "Сбросить фильтры",
"default_profile_picture": "Изображение профиля по умолчанию",
"light": "Свет",
"dark": "Темный",
"system": "Система"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Är du säker på att du vill återkalla API-nyckeln ”{apiKeyName}”? Alla integrationer som använder nyckeln slutar att fungera.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Är du säker på att du vill återkalla API-nyckeln ”{apiKeyName}”? Alla integrationer som använder nyckeln slutar att fungera.",
"last_used": "Senast använd", "last_used": "Senast använd",
"actions": "Åtgärder", "actions": "Åtgärder",
"images_updated_successfully": "Bilderna har uppdaterats", "images_updated_successfully": "Bilderna har uppdaterats. Det kan ta några minuter att uppdatera.",
"general": "Allmänt", "general": "Allmänt",
"configure_smtp_to_send_emails": "Aktivera e-postaviseringar för att varna användare när en inloggning upptäcks från en ny enhet eller plats.", "configure_smtp_to_send_emails": "Aktivera e-postaviseringar för att varna användare när en inloggning upptäcks från en ny enhet eller plats.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Lokalisering", "locale": "Lokalisering",
"ldap_id": "LDAP-ID", "ldap_id": "LDAP-ID",
"reauthentication": "Omverifiering", "reauthentication": "Omverifiering",
"clear_filters": "Rensa filter" "clear_filters": "Rensa filter",
"default_profile_picture": "Standardprofilbild",
"light": "Ljus",
"dark": "Mörk",
"system": "System"
} }

469
frontend/messages/tr.json Normal file
View File

@@ -0,0 +1,469 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "Hesabım",
"logout": ıkış Yap",
"confirm": "Onayla",
"docs": "Belgeler",
"key": "Anahtar",
"value": "Değer",
"remove_custom_claim": "Özel yetkiyi kaldır",
"add_custom_claim": "Özel yetki ekle",
"add_another": "Başka bir tane ekle",
"select_a_date": "Bir tarih seçin",
"select_file": "Dosya Seç",
"profile_picture": "Profil resmi",
"profile_picture_is_managed_by_ldap_server": "Profil resmi LDAP sunucusu tarafından yönetilmektedir ve burada değiştirilemez.",
"click_profile_picture_to_upload_custom": "Özel bir resim yüklemek için profil resmine tıklayın.",
"image_should_be_in_format": "Resim PNG veya JPEG formatında olmalıdır.",
"items_per_page": "Sayfa başına öğe sayısı",
"no_items_found": "Hiçbir öğe bulunamadı",
"select_items": "Öğeleri seçin...",
"search": "Ara...",
"expand_card": "Kartı genişlet",
"copied": "Kopyalandı",
"click_to_copy": "Kopyalamak için tıklayın",
"something_went_wrong": "Bir şeyler ters gitti",
"go_back_to_home": "Ana sayfaya geri dön",
"alternative_sign_in_methods": "Alternatif Giriş Yöntemleri",
"login_background": "Giriş arka planı",
"logo": "Logo",
"login_code": "Giriş Kodu",
"create_a_login_code_to_sign_in_without_a_passkey_once": "Kullanıcının bir kez parolasız oturum açmak için kullanabileceği bir giriş kodu oluşturun.",
"one_hour": "1 saat",
"twelve_hours": "12 saat",
"one_day": "1 gün",
"one_week": "1 hafta",
"one_month": "1 ay",
"expiration": "Son Geçerlilik Tarihi",
"generate_code": "Kod Oluştur",
"name": "İsim",
"browser_unsupported": "Desteklenmeyen tarayıcı",
"this_browser_does_not_support_passkeys": "Bu tarayıcı geçiş anahtarlarını desteklemiyor. Lütfen alternatif bir oturum açma yöntemi kullanın.",
"an_unknown_error_occurred": "Bilinmeyen bir hata oluştu",
"authentication_process_was_aborted": "Kimlik doğrulama süreci iptal edildi",
"error_occurred_with_authenticator": "Kimlik doğrulayıcıda bir hata oluştu",
"authenticator_does_not_support_discoverable_credentials": "Kimlik doğrulayıcı, keşfedilebilir kimlik bilgilerini desteklemiyor",
"authenticator_does_not_support_resident_keys": "Kimlik doğrulayıcı yerleşik anahtarları desteklemiyor",
"passkey_was_previously_registered": "Bu geçiş anahtarı daha önce kaydedilmiştir",
"authenticator_does_not_support_any_of_the_requested_algorithms": "Kimlik doğrulayıcı, talep edilen algoritmalardan hiçbirini desteklemiyor",
"authenticator_timed_out": "Kimlik doğrulayıcı zaman aşımına uğradı",
"critical_error_occurred_contact_administrator": "Kritik bir hata oluştu. Lütfen sistem yöneticinizle iletişime geçin.",
"sign_in_to": "{name} hesabına giriş yap",
"client_not_found": "İstemci bulunamadı",
"client_wants_to_access_the_following_information": "<b>{client}</b> aşağıdaki bilgilere erişmek istiyor:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "{appName} hesabınla <b>{client}</b> uygulamasına giriş yapmak istiyor musun?",
"email": "E-posta",
"view_your_email_address": "E-posta adresinizi görüntüleyin",
"profile": "Profil",
"view_your_profile_information": "Profil bilgilerinizi görüntüleyin",
"groups": "Gruplar",
"view_the_groups_you_are_a_member_of": "Üyesi olduğunuz grupları görüntüleyin",
"cancel": "İptal",
"sign_in": "Giriş yap",
"try_again": "Tekrar deneyin",
"client_logo": "İstemci Logosu",
"sign_out": ıkış yap",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "<b>{username}</b> adlı hesapla {appName} uygulamasından çıkış yapmak istiyor musun?",
"sign_in_to_appname": "{appName} hesabına giriş yap",
"please_try_to_sign_in_again": "Lütfen tekrar giriş yapmayı deneyin.",
"authenticate_with_passkey_to_access_account": "Hesabınıza erişmek için geçiş anahtarınızla kimlik doğrulaması yapın.",
"authenticate": "Kimliğini Doğrula",
"please_try_again": "Lütfen tekrar deneyin.",
"continue": "Devam et",
"alternative_sign_in": "Alternatif Giriş",
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Geçiş anahtarınıza erişiminiz yoksa, aşağıdaki yöntemlerden biriyle giriş yapabilirsiniz.",
"use_your_passkey_instead": "Geçiş anahtarınızı kullanmak ister misiniz?",
"email_login": "E-posta ile Giriş",
"enter_a_login_code_to_sign_in": "Giriş yapmak için bir kod girin.",
"sign_in_with_login_code": "Giriş kodu ile giriş yap",
"request_a_login_code_via_email": "E-posta ile giriş kodu isteyin.",
"go_back": "Geri dön",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Verilen e-posta adresine, sistemde mevcutsa bir e-posta gönderilmiştir.",
"enter_code": "Kodu girin",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Giriş kodunu içeren bir e-posta almak için e-posta adresinizi girin.",
"your_email": "E-posta adresiniz",
"submit": "Gönder",
"enter_the_code_you_received_to_sign_in": "Aldığınız kodu girin ve giriş yapın.",
"code": "Kod",
"invalid_redirect_url": "Geçersiz yönlendirme URL'si",
"audit_log": "Denetim Kaydı",
"users": "Kullanıcılar",
"user_groups": "Kullanıcı Grupları",
"oidc_clients": "OIDC İstemcileri",
"api_keys": "API Anahtarları",
"application_configuration": "Uygulama Yapılandırması",
"settings": "Ayarlar",
"update_pocket_id": "Pocket ID'yi Güncelle",
"powered_by": "Destekleyen",
"see_your_account_activities_from_the_last_3_months": "Son 3 aydaki hesap aktivitelerinizi görüntüleyin.",
"time": "Zaman",
"event": "Olay",
"approximate_location": "Yaklaşık Konum",
"ip_address": "IP Adresi",
"device": "Cihaz",
"client": "İstemci",
"unknown": "Bilinmeyen",
"account_details_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
"profile_picture_updated_successfully": "Profil resmi başarıyla güncellendi. Güncellenmesi birkaç dakika sürebilir.",
"account_settings": "Hesap Ayarları",
"passkey_missing": "Geçiş anahtarı eksik",
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Hesabınıza erişimi kaybetmemek için lütfen bir geçiş anahtarı ekleyin.",
"single_passkey_configured": "Tek geçiş anahtarı yapılandırıldı",
"it_is_recommended_to_add_more_than_one_passkey": "Birden fazla geçiş anahtarı eklemeniz önerilir, böylece hesabınıza erişimi kaybetmezsiniz.",
"account_details": "Hesap Bilgileri",
"passkeys": "Geçiş Anahtarları",
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Kendinizi doğrulamak için kullanabileceğiniz geçiş anahtarlarınızı yönetin.",
"add_passkey": "Geçiş Anahtarı Ekle",
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Geçiş anahtarı olmadan farklı bir cihazdan giriş yapmak için tek kullanımlık bir giriş kodu oluşturun.",
"create": "Oluştur",
"first_name": "İsim",
"last_name": "Soyadı",
"username": "Kullanıcı Adı",
"save": "Kaydet",
"username_can_only_contain": "Kullanıcı adı yalnızca küçük harfler, sayılar, alt çizgiler, noktalar, tireler ve '@' sembolleri içerebilir",
"username_must_start_with": "Kullanıcı adı bir harf veya sayısal karakterle başlamalıdır",
"username_must_end_with": "Kullanıcı adı bir harf veya sayısal karakterle bitmelidir",
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Aşağıdaki kodu kullanarak giriş yapın. Kod 15 dakika içinde süresi dolacaktır.",
"or_visit": "veya ziyaret edin",
"added_on": "Ekleme tarihi",
"rename": "Yeniden Adlandır",
"delete": "Sil",
"are_you_sure_you_want_to_delete_this_passkey": "Bu geçiş anahtarını silmek istediğinizden emin misiniz?",
"passkey_deleted_successfully": "Geçiş anahtarı başarıyla silindi",
"delete_passkey_name": "Sil {passkeyName}",
"passkey_name_updated_successfully": "Geçiş anahtarı adı başarıyla güncellendi",
"name_passkey": "Geçiş Anahtarı Adı",
"name_your_passkey_to_easily_identify_it_later": "Geçiş anahtarınızı daha sonra kolayca tanımlayabilmek için adlandırın.",
"create_api_key": "API Anahtarı Oluştur",
"add_a_new_api_key_for_programmatic_access": "Programatik erişim için <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>ye yeni bir API anahtarı ekle.",
"add_api_key": "API Anahtarı Ekle",
"manage_api_keys": "API Anahtarlarını Yönet",
"api_key_created": "API Anahtarı Oluşturuldu",
"for_security_reasons_this_key_will_only_be_shown_once": "Güvenlik nedenleriyle, bu anahtar yalnızca bir kez gösterilecektir. Lütfen güvenli bir şekilde saklayın.",
"description": "Açıklama",
"api_key": "API Anahtarı",
"close": "Kapat",
"name_to_identify_this_api_key": "Bu API anahtarını tanımlamak için bir isim girin.",
"expires_at": "Son Geçerlilik Tarihi",
"when_this_api_key_will_expire": "Bu API anahtarının süresinin ne zaman dolacağını belirtin.",
"optional_description_to_help_identify_this_keys_purpose": "Bu anahtarın amacını tanımlamaya yardımcı olacak isteğe bağlııklama.",
"expiration_date_must_be_in_the_future": "Son geçerlilik tarihi gelecekte olmalıdır",
"revoke_api_key": "API Anahtarını İptal Et",
"never": "Asla",
"revoke": "İptal Et",
"api_key_revoked_successfully": "API anahtarı başarıyla iptal edildi",
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "API anahtarı \"{apiKeyName}\" iptal etmek istediğinizden emin misiniz? Bu, bu anahtarı kullanan tüm entegrasyonları bozacaktır.",
"last_used": "Son Kullanım",
"actions": "Eylemler",
"images_updated_successfully": "Görüntüler başarıyla güncellendi. Güncelleme işlemi birkaç dakika sürebilir.",
"general": "Genel",
"configure_smtp_to_send_emails": "Yeni bir cihaz veya konumdan giriş tespit edildiğinde kullanıcıları bilgilendirmek için e-posta bildirimlerini etkinleştirin.",
"ldap": "LDAP",
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP ayarlarını yapılandırarak kullanıcıları ve grupları bir LDAP sunucusundan senkronize edin.",
"images": "Görseller",
"update": "Güncelle",
"email_configuration_updated_successfully": "E-posta yapılandırması başarıyla güncellendi",
"save_changes_question": "Değişiklikleri kaydetmek istiyor musunuz?",
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Test e-postası göndermeden önce değişiklikleri kaydetmelisiniz. Şimdi kaydetmek istiyor musunuz?",
"save_and_send": "Kaydet ve Gönder",
"test_email_sent_successfully": "Test e-postası, e-posta adresine başarıyla gönderildi.",
"failed_to_send_test_email": "Test e-postası gönderilemedi. Daha fazla bilgi için sunucu günlüklerini kontrol edin.",
"smtp_configuration": "SMTP Yapılandırması",
"smtp_host": "SMTP Sunucusu",
"smtp_port": "SMTP Portu",
"smtp_user": "SMTP kullanıcı adı",
"smtp_password": "SMTP Parolası",
"smtp_from": "SMTP Gönderen",
"smtp_tls_option": "SMTP TLS Seçeneği",
"email_tls_option": "E-posta TLS Seçeneği",
"skip_certificate_verification": "Sertifika Doğrulamasını Atla",
"this_can_be_useful_for_selfsigned_certificates": "Bu, kendi imzaladığınız sertifikalar için yararlı olabilir.",
"enabled_emails": "Etkin E-postalar",
"email_login_notification": "E-posta Giriş Bildirimi",
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Kullanıcı yeni bir cihazdan oturum açtığında e-posta gönder.",
"emai_login_code_requested_by_user": "Kullanıcı tarafından talep edilen e-posta giriş kodu",
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Kullanıcıların e-posta ile gönderilen bir giriş kodu ile oturum açmalarına izin verin. Bu, güvenliği önemli ölçüde azaltır çünkü kullanıcının e-postasına erişimi olan herkes giriş yapabilir.",
"email_login_code_from_admin": "Yönetici tarafından gönderilen e-posta giriş kodu",
"allows_an_admin_to_send_a_login_code_to_the_user": "Bir yöneticinin kullanıcıya e-posta ile giriş kodu göndermesine izin verir.",
"send_test_email": "Test e-postası gönder",
"application_configuration_updated_successfully": "Uygulama yapılandırması başarıyla güncellendi",
"application_name": "Uygulama Adı",
"session_duration": "Oturum Süresi",
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Kullanıcının tekrar oturum açması gereken süre, dakika cinsinden.",
"enable_self_account_editing": "Kullanıcının kendi hesabını düzenlemesini etkinleştir",
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Kullanıcıların kendi hesap bilgilerini düzenlemesine izin verilsin mi.",
"emails_verified": "E-postalar doğrulandı",
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Kullanıcının e-postasının OIDC istemcileri için doğrulanmış olarak işaretlenip işaretlenmeyeceği.",
"ldap_configuration_updated_successfully": "LDAP yapılandırması başarıyla güncellendi",
"ldap_disabled_successfully": "LDAP başarıyla devre dışı bırakıldı",
"ldap_sync_finished": "LDAP senkronizasyonu tamamlandı",
"client_configuration": "İstemci Yapılandırması",
"ldap_url": "LDAP URL",
"ldap_bind_dn": "LDAP Bind DN",
"ldap_bind_password": "LDAP Bind Parolası",
"ldap_base_dn": "LDAP Base DN",
"user_search_filter": "Kullanıcı Arama Filtresi",
"the_search_filter_to_use_to_search_or_sync_users": "Kullanıcıları aramak veya senkronize etmek için kullanılacak arama filtresi.",
"groups_search_filter": "Grupları Arama Filtresi",
"the_search_filter_to_use_to_search_or_sync_groups": "Grupları aramak veya senkronize etmek için kullanılacak arama filtresi.",
"attribute_mapping": "Öznitelik Haritalama",
"user_unique_identifier_attribute": "Kullanıcı Benzersiz Tanımlayıcı Özniteliği",
"the_value_of_this_attribute_should_never_change": "Bu özniteliğin değeri asla değişmemelidir.",
"username_attribute": "Kullanıcı Adı Özniteliği",
"user_mail_attribute": "Kullanıcı E-posta Özniteliği",
"user_first_name_attribute": "Kullanıcı Adı Özniteliği",
"user_last_name_attribute": "Kullanıcı Soyadı Özniteliği",
"user_profile_picture_attribute": "Kullanıcı Profil Resmi Özniteliği",
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Bu özniteliğin değeri ya bir URL, binary ya da base64 kodlu bir resim olabilir.",
"group_members_attribute": "Grup Üyeleri Özniteliği",
"the_attribute_to_use_for_querying_members_of_a_group": "Bir grubun üyelerini sorgulamak için kullanılacak öznitelik.",
"group_unique_identifier_attribute": "Grup Benzersiz Tanımlayıcı Özniteliği",
"group_rdn_attribute": "Grup RDN Özniteliği (DN içinde)",
"admin_group_name": "Yönetici Grup Adı",
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Bu grubun üyeleri Pocket ID'de yönetici ayrıcalıklarına sahip olacaktır.",
"disable": "Devre Dışı Bırak",
"sync_now": "Şimdi Senkronize et",
"enable": "Etkinleştir",
"user_created_successfully": "Kullanıcı başarıyla oluşturuldu",
"create_user": "Kullanıcı Oluştur",
"add_a_new_user_to_appname": "{appName} uygulamasına yeni bir kullanıcı ekleyin",
"add_user": "Kullanıcı Ekle",
"manage_users": "Kullanıcıları Yönet",
"admin_privileges": "Yönetici Ayrıcalıkları",
"admins_have_full_access_to_the_admin_panel": "Yöneticilerin yönetici paneline tam erişimi vardır.",
"delete_firstname_lastname": "Sil {firstName} {lastName}",
"are_you_sure_you_want_to_delete_this_user": "Bu kullanıcıyı silmek istediğinizden emin misiniz?",
"user_deleted_successfully": "Kullanıcı başarıyla silindi,",
"role": "Rol",
"source": "Kaynak",
"admin": "Yönetici",
"user": "Kullanıcı",
"local": "Yerel",
"toggle_menu": "Menüyü aç/kapat",
"edit": "Düzenle",
"user_groups_updated_successfully": "Kullanıcı grupları başarıyla güncellendi",
"user_updated_successfully": "Kullanıcı başarıyla güncellendi",
"custom_claims_updated_successfully": "Özel alanlar başarıyla güncellendi",
"back": "Geri",
"user_details_firstname_lastname": "Kullanıcı Detayları {firstName} {lastName}",
"manage_which_groups_this_user_belongs_to": "Bu kullanıcının ait olduğu grupları yönetin.",
"custom_claims": "Özel Alan",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Özel alan, bir kullanıcı hakkında ek bilgi depolamak için kullanılabilecek anahtar-değer çiftleridir. Bu alanlar, 'profil' kapsamı istendiğinde ID belirtecine dahil edilecektir.",
"user_group_created_successfully": "Kullanıcı grubu başarıyla oluşturuldu",
"create_user_group": "Kullanıcı Grubu Oluştur",
"create_a_new_group_that_can_be_assigned_to_users": "Kullanıcılara atanabilecek yeni bir grup oluşturun.",
"add_group": "Grup Ekle",
"manage_user_groups": "Kullanıcı Gruplarını Yönet",
"friendly_name": "Kullanıcı Dostu İsim",
"name_that_will_be_displayed_in_the_ui": "Kullanıcı arayüzünde görüntülenecek isim",
"name_that_will_be_in_the_groups_claim": "“Gruplar” alanında bulunacak ad",
"delete_name": "Sil {name}",
"are_you_sure_you_want_to_delete_this_user_group": "Bu kullanıcı grubunu silmek istediğinizden emin misiniz?",
"user_group_deleted_successfully": "Kullanıcı grubu başarıyla silindi",
"user_count": "Kullanıcı Sayısı",
"user_group_updated_successfully": "Kullanıcı grubu başarıyla güncellendi",
"users_updated_successfully": "Kullanıcılar başarıyla güncellendi",
"user_group_details_name": "Kullanıcı Grubu Detayları {name}",
"assign_users_to_this_group": "Bu gruba kullanıcı atayın.",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Özel alanlar, bir kullanıcı hakkında ek bilgileri depolamak için kullanılabilen anahtar-değer çiftleridir. Bu alanlar, 'profil' kapsamı talep edildiğinde kimlik belirtecine dahil edilecektir. Çakışma olması durumunda, kullanıcı üzerinde tanımlanan özel alanlar öncelikli olacaktır.",
"oidc_client_created_successfully": "OIDC istemcisi başarıyla oluşturuldu",
"create_oidc_client": "OIDC İstemcisi Oluştur",
"add_a_new_oidc_client_to_appname": "{appName} uygulamasına yeni bir oidc istemcisi ekleyin.",
"add_oidc_client": "OIDC İstemcisi Ekle",
"manage_oidc_clients": "OIDC İstemcilerini Yönet",
"one_time_link": "Tek Seferlik Bağlantı",
"use_this_link_to_sign_in_once": "Bu bağlantıyı bir kez oturum açmak için kullanın. Bu, henüz bir geçiş anahtarı eklememiş veya kaybetmiş kullanıcılar için gereklidir.",
"add": "Ekle",
"callback_urls": "Callback URL'leri",
"logout_callback_urls": ıkış Callback URL'leri",
"public_client": "Genel İstemci",
"public_clients_description": "Genel istemciler bir istemci sırrına sahip değildir. Güvenlik anahtarlarının güvenli bir şekilde saklanamayacağı mobil, web ve yerel uygulamalar için tasarlanmıştır.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Genel Anahtar Kod Değişimi, CSRF ve yetkilendirme kodu ele geçirme saldırılarını önlemek için bir güvenlik özelliğidir.",
"requires_reauthentication": "Yeniden Kimlik Doğrulama Gerekir",
"requires_users_to_authenticate_again_on_each_authorization": "Kullanıcıların her yetkilendirmede tekrar kimlik doğrulaması yapmasını gerektirir, oturum açmış olsalar bile",
"name_logo": "{name} logosu",
"change_logo": "Logoyu Değiştir",
"upload_logo": "Logo Yükle",
"remove_logo": "Logoyu Kaldır",
"are_you_sure_you_want_to_delete_this_oidc_client": "Bu OIDC istemcisini silmek istediğinizden emin misiniz?",
"oidc_client_deleted_successfully": "OIDC istemcisi başarıyla silindi",
"authorization_url": "Yetkilendirme URL'si",
"oidc_discovery_url": "OIDC Discovery URL'si",
"token_url": "Token URL'si",
"userinfo_url": "Kullanıcı bilgisi URL'si",
"logout_url": ıkış URL'si",
"certificate_url": "Sertifika URL'si",
"enabled": "Etkin",
"disabled": "Devre dışı",
"oidc_client_updated_successfully": "OIDC istemcisi başarıyla güncellendi",
"create_new_client_secret": "Yeni istemci sırrı oluştur",
"are_you_sure_you_want_to_create_a_new_client_secret": "Yeni bir istemci sırrı oluşturmak istediğinizden emin misiniz? Eskisi geçersiz kılınacaktır.",
"generate": "Üret",
"new_client_secret_created_successfully": "Yeni istemci sırrı başarıyla oluşturuldu",
"allowed_user_groups_updated_successfully": "İzin verilen kullanıcı grupları başarıyla güncellendi",
"oidc_client_name": "OIDC İstemcisi {name}",
"client_id": "İstemci kimliği",
"client_secret": "İstemci sırrı",
"show_more_details": "Daha fazla detay göster",
"allowed_user_groups": "İzin Verilen Kullanıcı Grupları",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Bu istemciye kullanıcı grupları ekleyerek bu gruplardaki kullanıcılara erişimi kısıtlayın. Hiçbir kullanıcı grubu seçilmezse, tüm kullanıcılar bu istemciye erişebilecektir.",
"favicon": "Favicon",
"light_mode_logo": "Açık Mod Logo",
"dark_mode_logo": "Karanlık Mod Logo",
"background_image": "Arkaplan Resmi",
"language": "Dil",
"reset_profile_picture_question": "Profil resmini sıfırlamak istiyor musunuz?",
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Bu, yüklenen resmi kaldıracak ve profil resmini varsayılan ayarına sıfırlayacaktır. Devam etmek istiyor musunuz?",
"reset": "Sıfırla",
"reset_to_default": "Varsayılan Ayara Sıfırla",
"profile_picture_has_been_reset": "Profil resmi sıfırlandı. Güncellenmesi birkaç dakika sürebilir.",
"select_the_language_you_want_to_use": "Kullanmak istediğiniz dili seçin. Lütfen bazı metinlerin otomatik olarak çevrilebileceğini ve hatalı olabileceğini unutmayın.",
"contribute_to_translation": "Bir sorunla karşılaşırsan, <link href='https://crowdin.com/project/pocket-id'>Crowdin</link> üzerinden çeviriye katkı sağlayabilirsin.",
"personal": "Kişisel",
"global": "Genel",
"all_users": "Tüm Kullanıcılar",
"all_events": "Tüm Etkinlikler",
"all_clients": "Tüm İstemciler",
"all_locations": "Tüm Konumlar",
"global_audit_log": "Genel Denetim Kaydı",
"see_all_account_activities_from_the_last_3_months": "Son 3 ay içindeki tüm kullanıcı etkinliklerini görüntüle.",
"token_sign_in": "Token ile Giriş",
"client_authorization": "İstemci Yetkilendirmesi",
"new_client_authorization": "Yeni İstemci Yetkilendirmesi",
"disable_animations": "Animasyonları Devre Dışı Bırak",
"turn_off_ui_animations": "Kullanıcı arayüzü animasyonlarını kapatın.",
"user_disabled": "Hesap Devre Dışı",
"disabled_users_cannot_log_in_or_use_services": "Hesapları devre dışı olan kullanıcılar, giriş yapamaz veya hizmetleri kullanamaz.",
"user_disabled_successfully": "Kullanıcı başarıyla devre dışı bırakıldı.",
"user_enabled_successfully": "Kullanıcı başarıyla etkinleştirildi.",
"status": "Durum",
"disable_firstname_lastname": "{firstName} {lastName} hesabını devre dışı bırak",
"are_you_sure_you_want_to_disable_this_user": "Bu kullanıcıyı devre dışı bırakmak istediğinizden emin misiniz? Devre dışı bırakılan kullanıcılar giriş yapamaz veya hizmetleri kullanamaz.",
"ldap_soft_delete_users": "Hesapları devre dışı olan kullanıcıları LDAP'tan koru.",
"ldap_soft_delete_users_description": "Etkinleştirildiğinde, LDAP'tan kaldırılan kullanıcılar sistemden silinmek yerine hesapları devre dışı bırakılacaktır.",
"login_code_email_success": "Giriş kodu kullanıcıya gönderildi.",
"send_email": "E-posta Gönder",
"show_code": "Kodu Göster",
"callback_url_description": "URL'ler, istemciniz tarafından sağlanır. Boş bırakılırsa otomatik olarak eklenecektir. Wildcard (*) karakterleri desteklenmektedir, ancak daha iyi güvenlik için kaçınılması önerilir.",
"logout_callback_url_description": ıkış URL'leri, istemciniz tarafından sağlanır. Boş bırakılırsa otomatik olarak eklenecektir. Wildcard (*) karakterleri desteklenmektedir, ancak daha iyi güvenlik için kaçınılması önerilir.",
"api_key_expiration": "API Anahtarının Geçerlilik Süresi",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API anahtarları sona ermek üzere olduğunda kullanıcıya bir e-posta gönderin.",
"authorize_device": "Cihazı Yetkilendir",
"the_device_has_been_authorized": "Cihaz yetkilendirildi.",
"enter_code_displayed_in_previous_step": "Önceki adımda görüntülenen kodu girin.",
"authorize": "Yetkilendir",
"federated_client_credentials": "Birleştirilmiş İstemci Kimlik Bilgileri",
"federated_client_credentials_description": "Birleşik istemci kimlik bilgilerini kullanarak, üçüncü taraf otoriteleri tarafından verilen JWT token'ları kullanarak OIDC istemcilerinin kimliklerini doğrulayabilirsiniz.",
"add_federated_client_credential": "Birleştirilmiş İstemci Kimlik Bilgisi Ekle",
"add_another_federated_client_credential": "Başka bir birleştirilmiş istemci kimlik bilgisi ekle",
"oidc_allowed_group_count": "İzin Verilen Grup Sayısı",
"unrestricted": "Kısıtlanmamış",
"show_advanced_options": "Gelişmiş Seçenekleri Göster",
"hide_advanced_options": "Gelişmiş Seçenekleri Gizle",
"oidc_data_preview": "OIDC Veri Önizlemesi",
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Farklı kullanıcılar için gönderilecek OIDC verilerini önizleyin",
"id_token": "Kimlik Tokeni",
"access_token": "Erişim Tokeni",
"userinfo": "Kullanıcı bilgisi",
"id_token_payload": "Kimlik Belirteci Yükü",
"access_token_payload": "Erişim Tokenı İçeriği",
"userinfo_endpoint_response": "Kullanıcı Bilgisi Uç Noktası Yanıtı",
"copy": "Kopyala",
"no_preview_data_available": "Önizleme verisi mevcut değil",
"copy_all": "Tümünü Kopyala",
"preview": "Önizleme",
"preview_for_user": "{name} için Önizleme",
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Bu kullanıcı için gönderilecek OIDC verilerini önizleyin",
"show": "Göster",
"select_an_option": "Bir seçenek seçin",
"select_user": "Kullanıcı Seç",
"error": "Hata",
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Pocket ID'nin görünümünü özelleştirmek için bir vurgu rengi seçin.",
"accent_color": "Vurgu Rengi",
"custom_accent_color": "Özel Vurgu Rengi",
"custom_accent_color_description": "Geçerli CSS renk formatlarını kullanarak (örn. hex, rgb, hsl) özel bir renk girin.",
"color_value": "Renk Değeri",
"apply": "Uygula",
"signup_token": "Kayıt Olma Tokeni",
"create_a_signup_token_to_allow_new_user_registration": "Yeni kullanıcı kaydına izin vermek için bir kayıt token'i oluşturun.",
"usage_limit": "Kullanım Limiti",
"number_of_times_token_can_be_used": "Kayıt token'inin kullanılabileceği maksimum sayıyı belirtin.",
"expires": "Sona Erme Tarihi",
"signup": "Kaydol",
"user_creation": "Kullanıcı Oluşturma",
"configure_user_creation": "Kullanıcı oluşturma ayarlarını yönetin, kayıt yöntemleri ve yeni kullanıcılar için varsayılan izinler dahil.",
"user_creation_groups_description": "Bu grupları otomatik olarak yeni kullanıcılara atayın.",
"user_creation_claims_description": "Bu özel alanları otomatik olarak yeni kullanıcılara atayın.",
"user_creation_updated_successfully": "Kullanıcı oluşturma ayarları başarıyla güncellendi.",
"signup_disabled_description": "Kullanıcı kayıtları tamamen devre dışı bırakılmıştır. Sadece yöneticiler yeni kullanıcı hesapları oluşturabilir.",
"signup_requires_valid_token": "Bir hesap oluşturmak için geçerli bir kayıt token'i gereklidir",
"validating_signup_token": "Kayıt token'i doğrulanıyor",
"go_to_login": "Giriş yap",
"signup_to_appname": "{appName} Kayıt Ol",
"create_your_account_to_get_started": "Başlamak için hesabınızı oluşturun.",
"initial_account_creation_description": "Başlamak için lütfen hesabınızı oluşturun. Daha sonra bir geçiş anahtarı ayarlayabileceksiniz.",
"setup_your_passkey": "Geçiş Anahtarınızı Ayarlayın",
"create_a_passkey_to_securely_access_your_account": "Hesabınıza güvenli bir şekilde erişmek için bir geçiş anahtarı oluşturun. Bu, oturum açmanın birincil yolu olacaktır.",
"skip_for_now": "Şimdilik Atla",
"account_created": "Hesap Oluşturuldu",
"enable_user_signups": "Kullanıcıların hesap oluşturmasını Etkinleştir",
"enable_user_signups_description": "Kullanıcıların Pocket ID'de yeni hesaplar için nasıl kaydolabileceğini belirleyin.",
"user_signups_are_disabled": "Kullanıcıların hesap oluşturma işlemleri şimdilik devre dışı",
"create_signup_token": "Kayıt Token'i Oluştur",
"view_active_signup_tokens": "Aktif Kayıt Token'lerini Görüntüle",
"manage_signup_tokens": "Kayıt Token'lerini Yönet",
"view_and_manage_active_signup_tokens": "Aktif kayıt token'lerini görüntüleyin ve yönetin.",
"signup_token_deleted_successfully": "Kayıt token'i başarıyla silindi.",
"expired": "Süresi Dolmuş",
"used_up": "Tükenmiş",
"active": "Aktif",
"usage": "Kullanım",
"created": "Oluşturuldu",
"token": "Token",
"loading": "Yükleniyor",
"delete_signup_token": "Kayıt Token'ini Sil",
"are_you_sure_you_want_to_delete_this_signup_token": "Bu kayıt token'ini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"signup_with_token": "Kayıt olma token'i ile kaydol",
"signup_with_token_description": "Kullanıcılar yalnızca bir yönetici tarafından oluşturulan geçerli bir kayıt token'i ile kaydolabilir.",
"signup_open": "Hesap oluşturma açık",
"signup_open_description": "Herkes kısıtlama olmaksızın yeni bir hesap oluşturabilir.",
"of": "of",
"skip_passkey_setup": "Geçiş Anahtarı Kurulumunu Atla",
"skip_passkey_setup_description": "Bir geçiş anahtarı ayarlamanız şiddetle önerilir çünkü bir tane olmadan, oturum süresi dolduğunda hesabınıza erişiminiz engellenecektir.",
"my_apps": "Uygulamalarım",
"no_apps_available": "Kullanılabilir uygulama yok",
"contact_your_administrator_for_app_access": "Uygulamalara erişim için yöneticinizle iletişime geçin.",
"launch": "Başlat",
"client_launch_url": "İstemci Başlatma URL'si",
"client_launch_url_description": "Kullanıcıların Uygulamalarım sayfasından uygulamayı başlattığında açılacak URL.",
"client_name_description": "Pocket ID arayüzünde gösterilen istemci adı.",
"revoke_access": "Erişimi iptal et",
"revoke_access_description": "Erişimi <b>{clientName}</b> için iptal et. <b>{clientName}</b> artık hesap bilgilerinize erişemeyecek.",
"revoke_access_successful": "Erişim {clientName} başarıyla iptal edildi.",
"last_signed_in_ago": "Son oturum açma {time} önce",
"invalid_client_id": "İstemci kimliği yalnızca harfler, rakamlar, alt çizgi ve tire içerebilir",
"custom_client_id_description": "Uygulamanızın gerektirmesi durumunda özel bir istemci kimliği ayarlayın. Aksi takdirde, rastgele bir tane oluşturmak için boş bırakın.",
"generated": "Oluşturuldu",
"administration": "Yönetim",
"group_rdn_attribute_description": "Grupların ayırt edici adında (DN) kullanılan öznitelik.",
"display_name_attribute": "Görünen Ad Özniteliği",
"display_name": "Görünen Ad",
"configure_application_images": "Uygulama Görsellerini Yapılandır",
"ui_config_disabled_info_title": "UI Yapılandırması Devre Dışı",
"ui_config_disabled_info_description": "Kullanıcı arayüzü yapılandırması devre dışı bırakıldı çünkü uygulama yapılandırma ayarları ortam değişkenleri aracılığıyla yönetiliyor. Bazı ayarlar düzenlenemeyebilir.",
"logo_from_url_description": "Doğrudan bir resim URLsi (svg, png, webp) yapıştırın. İkonları <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> veya <link href=\"https://dashboardicons.com\">Dashboard Icons</link>adreslerinden bulabilirsiniz.",
"invalid_url": "Geçersiz URL",
"require_user_email": "E-posta Adresi Gerektir",
"require_user_email_description": "Kullanıcıların bir e-posta adresine sahip olmasını gerektirir. Devre dışı bırakılırsa, e-posta adresi olmayan kullanıcılar, bir e-posta adresi gerektiren özellikleri kullanamayacaktır.",
"view": "Görüntüle",
"toggle_columns": "Sütünları değiştir",
"locale": "Yerel",
"ldap_id": "LDAP Kimliği",
"reauthentication": "Yeniden Kimlik Doğrulama",
"clear_filters": "Filtreleri Temizle",
"default_profile_picture": "Varsayılan Profil Resmi",
"light": "Işık",
"dark": "Karanlık",
"system": "Sistem"
}

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Ви впевнені, що хочете анулювати API-ключ «{apiKeyName}»? Це призведе до зупинки всіх інтеграцій, які використовують цей ключ.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Ви впевнені, що хочете анулювати API-ключ «{apiKeyName}»? Це призведе до зупинки всіх інтеграцій, які використовують цей ключ.",
"last_used": "Останнє використання", "last_used": "Останнє використання",
"actions": "Дії", "actions": "Дії",
"images_updated_successfully": "Зображення успішно оновлено", "images_updated_successfully": "Зображення успішно оновлено. Оновлення може зайняти кілька хвилин.",
"general": "Загальне", "general": "Загальне",
"configure_smtp_to_send_emails": "Увімкніть сповіщення електронною поштою, щоб повідомляти користувачів про вхід з нового пристрою або місцеперебування.", "configure_smtp_to_send_emails": "Увімкніть сповіщення електронною поштою, щоб повідомляти користувачів про вхід з нового пристрою або місцеперебування.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Локаль", "locale": "Локаль",
"ldap_id": "LDAP-ідентифікатор", "ldap_id": "LDAP-ідентифікатор",
"reauthentication": "Повторна аутентифікація", "reauthentication": "Повторна аутентифікація",
"clear_filters": "Очистити фільтри" "clear_filters": "Очистити фільтри",
"default_profile_picture": "Стандартне зображення профілю",
"light": "Світло",
"dark": "Темний",
"system": "Система"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bạn có chắc chắn muốn hủy API Key \"{apiKeyName}\" không? Việc này sẽ làm gián đoạn tất cả các tích hợp sử dụng khóa này.", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bạn có chắc chắn muốn hủy API Key \"{apiKeyName}\" không? Việc này sẽ làm gián đoạn tất cả các tích hợp sử dụng khóa này.",
"last_used": "Lần Sử Dụng Cuối", "last_used": "Lần Sử Dụng Cuối",
"actions": "Hành động", "actions": "Hành động",
"images_updated_successfully": "Đã cập nhật hình ảnh thành công", "images_updated_successfully": "Hình ảnh đã được cập nhật thành công. Quá trình cập nhật có thể mất vài phút.",
"general": "Tổng quan", "general": "Tổng quan",
"configure_smtp_to_send_emails": "Bật thông báo email để thông báo cho người dùng khi phát hiện đăng nhập từ thiết bị hoặc vị trí mới.", "configure_smtp_to_send_emails": "Bật thông báo email để thông báo cho người dùng khi phát hiện đăng nhập từ thiết bị hoặc vị trí mới.",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "Vùng", "locale": "Vùng",
"ldap_id": "ID LDAP", "ldap_id": "ID LDAP",
"reauthentication": "Xác thực lại", "reauthentication": "Xác thực lại",
"clear_filters": "Xóa bộ lọc" "clear_filters": "Xóa bộ lọc",
"default_profile_picture": "Ảnh đại diện mặc định",
"light": "Ánh sáng",
"dark": "Tối",
"system": "Hệ thống"
} }

View File

@@ -19,7 +19,7 @@
"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": "出了点问题",
@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您确定要撤销 API 密钥 \"{apiKeyName}\" 吗?这将中断使用此密钥的任何集成。", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您确定要撤销 API 密钥 \"{apiKeyName}\" 吗?这将中断使用此密钥的任何集成。",
"last_used": "上次使用时间", "last_used": "上次使用时间",
"actions": "操作", "actions": "操作",
"images_updated_successfully": "成功更新图片", "images_updated_successfully": "图片更新成功更新过程可能需要几分钟时间。",
"general": "常规", "general": "常规",
"configure_smtp_to_send_emails": "启用电子邮件通知,当检测到来自新设备或新位置的登录时提醒用户。", "configure_smtp_to_send_emails": "启用电子邮件通知,当检测到来自新设备或新位置的登录时提醒用户。",
"ldap": "LDAP", "ldap": "LDAP",
@@ -394,7 +394,7 @@
"user_creation": "用户创建", "user_creation": "用户创建",
"configure_user_creation": "管理用户创建设置,包括注册方式和新用户的默认权限。", "configure_user_creation": "管理用户创建设置,包括注册方式和新用户的默认权限。",
"user_creation_groups_description": "新用户注册时自动分配的群组。", "user_creation_groups_description": "新用户注册时自动分配的群组。",
"user_creation_claims_description": "用户注册时自动为新用户分配这些自定义声明。", "user_creation_claims_description": "用户注册时自动分配的自定义声明。",
"user_creation_updated_successfully": "用户创建设置已成功更新。", "user_creation_updated_successfully": "用户创建设置已成功更新。",
"signup_disabled_description": "已完全禁止新用户注册。只有管理员可以创建新账户。", "signup_disabled_description": "已完全禁止新用户注册。只有管理员可以创建新账户。",
"signup_requires_valid_token": "必须输入有效注册令牌才能注册新账户", "signup_requires_valid_token": "必须输入有效注册令牌才能注册新账户",
@@ -435,21 +435,21 @@
"no_apps_available": "没有可用应用", "no_apps_available": "没有可用应用",
"contact_your_administrator_for_app_access": "请联系您的管理员以获取应用的访问权限。", "contact_your_administrator_for_app_access": "请联系您的管理员以获取应用的访问权限。",
"launch": "启动", "launch": "启动",
"client_launch_url": "客户发布链接", "client_launch_url": "客户端启动网址",
"client_launch_url_description": "当用户从“我的应用”页面启动应用时将打开的 URL。", "client_launch_url_description": "当用户从“我的应用”页面启动应用时将打开的 URL。",
"client_name_description": "在Pocket ID用户界面中显示的客户端名称。", "client_name_description": "在Pocket ID用户界面中显示的客户端名称。",
"revoke_access": "撤销访问权限", "revoke_access": "撤销访问权限",
"revoke_access_description": "撤销 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。", "revoke_access_description": "撤销 <b>{clientName}</b>的访问权限。 <b>{clientName}</b>将无法再访问您的账户信息。",
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。", "revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。",
"last_signed_in_ago": "最后一次登录 {time} 前", "last_signed_in_ago": "最后一次登录 {time} 前",
"invalid_client_id": "客户端 ID 只能包含字母、数字、下划线和连字符", "invalid_client_id": "客户端 ID 只能包含字母、数字、下划线和连字符",
"custom_client_id_description": "此处可根据应用需要设置自定义客户端 ID。留空随机生成。", "custom_client_id_description": "此处可根据应用需要设置自定义客户端 ID。留空随机生成。",
"generated": "已生成", "generated": "已生成",
"administration": "管理员选项", "administration": "管理员选项",
"group_rdn_attribute_description": "在组的区分名称DN中使用的属性。", "group_rdn_attribute_description": "在组的区分名称DN中使用的属性。",
"display_name_attribute": "显示名称属性", "display_name_attribute": "显示名称属性",
"display_name": "显示名称", "display_name": "显示名称",
"configure_application_images": "配置应用程序图标", "configure_application_images": "配置应用图标",
"ui_config_disabled_info_title": "用户界面配置已禁用", "ui_config_disabled_info_title": "用户界面配置已禁用",
"ui_config_disabled_info_description": "由于应用配置设置已通过环境变量设定,用户界面配置已禁用。某些设置可能无法编辑。", "ui_config_disabled_info_description": "由于应用配置设置已通过环境变量设定,用户界面配置已禁用。某些设置可能无法编辑。",
"logo_from_url_description": "粘贴直接图片URLsvg、png、webp格式。<link href=\"https://selfh.st/icons\">可在Selfh.st图标库</link>或<link href=\"https://dashboardicons.com\">仪表盘图标库</link>中查找图标。", "logo_from_url_description": "粘贴直接图片URLsvg、png、webp格式。<link href=\"https://selfh.st/icons\">可在Selfh.st图标库</link>或<link href=\"https://dashboardicons.com\">仪表盘图标库</link>中查找图标。",
@@ -461,5 +461,9 @@
"locale": "区域设置", "locale": "区域设置",
"ldap_id": "LDAP标识符", "ldap_id": "LDAP标识符",
"reauthentication": "重新认证", "reauthentication": "重新认证",
"clear_filters": "清除筛选条件" "clear_filters": "清除筛选条件",
"default_profile_picture": "默认个人资料图片",
"light": "光",
"dark": "黑暗",
"system": "系统"
} }

View File

@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您確定要撤銷 API 金鑰「{apiKeyName}」嗎?這將會中斷所有使用此金鑰的整合。", "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您確定要撤銷 API 金鑰「{apiKeyName}」嗎?這將會中斷所有使用此金鑰的整合。",
"last_used": "上次使用", "last_used": "上次使用",
"actions": "操作", "actions": "操作",
"images_updated_successfully": "圖片更新成功", "images_updated_successfully": "圖片已成功更新。更新過程可能需要數分鐘。",
"general": "一般", "general": "一般",
"configure_smtp_to_send_emails": "啟用電子郵件通知以提醒使用者有來自新裝置或位置的登入。", "configure_smtp_to_send_emails": "啟用電子郵件通知以提醒使用者有來自新裝置或位置的登入。",
"ldap": "LDAP", "ldap": "LDAP",
@@ -461,5 +461,9 @@
"locale": "地區", "locale": "地區",
"ldap_id": "LDAP 識別碼", "ldap_id": "LDAP 識別碼",
"reauthentication": "重新驗證", "reauthentication": "重新驗證",
"clear_filters": "清除篩選條件" "clear_filters": "清除篩選條件",
"default_profile_picture": "預設個人資料照片",
"light": "光",
"dark": "黑暗",
"system": "系統"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "1.14.0", "version": "1.15.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -15,6 +15,7 @@
"pt-BR", "pt-BR",
"ru", "ru",
"sv", "sv",
"tr",
"uk", "uk",
"vi", "vi",
"zh-CN", "zh-CN",

View File

@@ -36,6 +36,8 @@
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--warning: oklch(0.96 0.06 85);
--warning-foreground: oklch(0.32 0.07 70);
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
@@ -70,6 +72,8 @@
--accent: oklch(0.269 0 0); --accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--warning: oklch(0.42 0.09 70);
--warning-foreground: oklch(0.93 0.03 85);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
@@ -114,6 +118,8 @@
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground); --color-destructive-foreground: var(--destructive-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-radius: var(--radius); --color-radius: var(--radius);
--color-sidebar-background: var(--sidebar-background); --color-sidebar-background: var(--sidebar-background);

View File

@@ -1,12 +1,25 @@
<!doctype html> <!doctype html>
<html lang="%lang%"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/api/application-images/favicon" /> <link rel="icon" href="/api/application-images/favicon" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<link rel="manifest" href="/app.webmanifest" /> <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% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@@ -4,6 +4,7 @@
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import Logo from '../logo.svelte'; import Logo from '../logo.svelte';
import HeaderAvatar from './header-avatar.svelte'; import HeaderAvatar from './header-avatar.svelte';
import ModeSwitcher from './mode-switcher.svelte';
const authUrls = [ const authUrls = [
/^\/authorize$/, /^\/authorize$/,
@@ -38,6 +39,7 @@
{/if} {/if}
</div> </div>
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<ModeSwitcher />
{#if $userStore?.id} {#if $userStore?.id}
<HeaderAvatar /> <HeaderAvatar />
{/if} {/if}

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import SunIcon from '@lucide/svelte/icons/sun';
import MoonIcon from '@lucide/svelte/icons/moon';
import { mode, resetMode, setMode } from 'mode-watcher';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { m } from '$lib/paraglide/messages';
const isDark = $derived(mode.current === 'dark');
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
<SunIcon
class="h-[1.2rem] w-[1.2rem] !transition-all {isDark
? '-rotate-90 scale-0'
: 'rotate-0 scale-100'}"
/>
<MoonIcon
class="absolute h-[1.2rem] w-[1.2rem] !transition-all {isDark
? 'rotate-0 scale-100'
: 'rotate-90 scale-0'}"
/>
<span class="sr-only">Toggle theme</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={() => setMode('light')}>{m.light()}</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => setMode('dark')}>{m.dark()}</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => resetMode()}>{m.system()}</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -166,6 +166,12 @@
return filters; return filters;
} }
function getPrimaryAction(item: T) {
if (!actions) return null;
const availableActions = actions(item).filter((a) => a.primary);
return availableActions.length > 0 ? () => availableActions[0].onClick(item) : null;
}
export async function refresh() { export async function refresh() {
items = await fetchCallback(requestOptions); items = await fetchCallback(requestOptions);
changePageState(items.pagination.currentPage); changePageState(items.pagination.currentPage);
@@ -248,7 +254,13 @@
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each items.data as item} {#each items.data as item}
<Table.Row class={selectedIds?.includes(item.id) ? 'bg-muted/20' : ''}> <Table.Row
class={{
'bg-muted/20': selectedIds?.includes(item.id),
'cursor-pointer': getPrimaryAction(item)
}}
onclick={getPrimaryAction(item)}
>
{#if selectedIds} {#if selectedIds}
<Table.Cell class="w-12"> <Table.Cell class="w-12">
<Checkbox <Checkbox

View File

@@ -10,7 +10,7 @@
destructive: destructive:
'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current', 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current',
warning: warning:
'bg-amber-100 text-amber-900 dark:bg-amber-900 dark:text-amber-100 [&>svg]:text-amber-900 dark:[&>svg]:text-amber-100' 'bg-warning text-warning-foreground border-warning/40 [&>svg]:text-warning-foreground'
} }
}, },
defaultVariants: { defaultVariants: {
@@ -67,7 +67,7 @@
> >
{@render children?.()} {@render children?.()}
{#if dismissibleId} {#if dismissibleId}
<button onclick={dismiss} class="absolute top-0 right-0 m-3 text-black dark:text-white" <button onclick={dismiss} class="absolute right-0 top-0 m-3 text-black dark:text-white"
><LucideX class="size-4" /></button ><LucideX class="size-4" /></button
> >
{/if} {/if}

View File

@@ -55,9 +55,13 @@
disabled, disabled,
isLoading = false, isLoading = false,
autofocus = false, autofocus = false,
onclick,
usePromiseLoading = false,
children, children,
...restProps ...restProps
}: ButtonProps = $props(); }: ButtonProps & {
usePromiseLoading?: boolean;
} = $props();
onMount(async () => { onMount(async () => {
// Using autofocus can be bad for a11y, but in the case of Pocket ID is only used responsibly in places where there are not many choices, and on buttons only where there's descriptive text // Using autofocus can be bad for a11y, but in the case of Pocket ID is only used responsibly in places where there are not many choices, and on buttons only where there's descriptive text
@@ -66,6 +70,19 @@
setTimeout(() => ref?.focus(), 100); setTimeout(() => ref?.focus(), 100);
} }
}); });
async function handleOnClick(event: any) {
if (usePromiseLoading && onclick) {
isLoading = true;
try {
await onclick(event);
} finally {
isLoading = false;
}
} else {
onclick?.(event);
}
}
</script> </script>
{#if href} {#if href}
@@ -91,6 +108,7 @@
class={cn(buttonVariants({ variant, size }), className)} class={cn(buttonVariants({ variant, size }), className)}
{type} {type}
disabled={disabled || isLoading} disabled={disabled || isLoading}
onclick={handleOnClick}
{...restProps} {...restProps}
> >
{#if isLoading} {#if isLoading}

View File

@@ -1,5 +1,12 @@
import userStore from '$lib/stores/user-store';
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration'; import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-image-util'; import {
cachedApplicationLogo,
cachedBackgroundImage,
cachedDefaultProfilePicture,
cachedProfilePicture
} from '$lib/utils/cached-image-util';
import { get } from 'svelte/store';
import APIService from './api-service'; import APIService from './api-service';
export default class AppConfigService extends APIService { export default class AppConfigService extends APIService {
@@ -24,14 +31,14 @@ export default class AppConfigService extends APIService {
updateFavicon = async (favicon: File) => { updateFavicon = async (favicon: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', favicon!); formData.append('file', favicon);
await this.api.put(`/application-images/favicon`, formData); await this.api.put(`/application-images/favicon`, formData);
} };
updateLogo = async (logo: File, light = true) => { updateLogo = async (logo: File, light = true) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', logo!); formData.append('file', logo);
await this.api.put(`/application-images/logo`, formData, { await this.api.put(`/application-images/logo`, formData, {
params: { light } params: { light }
@@ -39,6 +46,14 @@ export default class AppConfigService extends APIService {
cachedApplicationLogo.bustCache(light); cachedApplicationLogo.bustCache(light);
}; };
updateDefaultProfilePicture = async (defaultProfilePicture: File) => {
const formData = new FormData();
formData.append('file', defaultProfilePicture);
await this.api.put(`/application-images/default-profile-picture`, formData);
cachedDefaultProfilePicture.bustCache();
};
updateBackgroundImage = async (backgroundImage: File) => { updateBackgroundImage = async (backgroundImage: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', backgroundImage!); formData.append('file', backgroundImage!);
@@ -47,6 +62,12 @@ export default class AppConfigService extends APIService {
cachedBackgroundImage.bustCache(); cachedBackgroundImage.bustCache();
}; };
deleteDefaultProfilePicture = async () => {
await this.api.delete('/application-images/default-profile-picture');
cachedDefaultProfilePicture.bustCache();
cachedProfilePicture.bustCache(get(userStore)!.id);
};
sendTestEmail = async () => { sendTestEmail = async () => {
await this.api.post('/application-configuration/test-email'); await this.api.post('/application-configuration/test-email');
}; };

View File

@@ -22,5 +22,6 @@ export type AdvancedTableAction<T> = {
variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost'; variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost';
onClick: (item: T) => void; onClick: (item: T) => void;
hidden?: boolean; hidden?: boolean;
primary?: boolean;
disabled?: boolean; disabled?: boolean;
}; };

View File

@@ -20,6 +20,13 @@ export const cachedApplicationLogo: CachableImage = {
} }
}; };
export const cachedDefaultProfilePicture: CachableImage = {
getUrl: () =>
getCachedImageUrl(new URL('/api/application-images/default-profile-picture', window.location.origin)),
bustCache: () =>
bustImageCache(new URL('/api/application-images/default-profile-picture', window.location.origin))
};
export const cachedBackgroundImage: CachableImage = { export const cachedBackgroundImage: CachableImage = {
getUrl: () => getUrl: () =>
getCachedImageUrl(new URL('/api/application-images/background', window.location.origin)), getCachedImageUrl(new URL('/api/application-images/background', window.location.origin)),

View File

@@ -9,6 +9,7 @@ import { z } from 'zod/v4';
export async function setLocale(locale: Locale, reload = true) { export async function setLocale(locale: Locale, reload = true) {
await setLocaleForLibraries(locale); await setLocaleForLibraries(locale);
setParaglideLocale(locale, { reload }); setParaglideLocale(locale, { reload });
document.documentElement.lang = locale;
} }
export async function setLocaleForLibraries( export async function setLocaleForLibraries(

View File

@@ -5,7 +5,7 @@
import { Toaster } from '$lib/components/ui/sonner'; import { Toaster } from '$lib/components/ui/sonner';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import type { Snippet } from 'svelte'; import { type Snippet } from 'svelte';
import '../app.css'; import '../app.css';
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';

View File

@@ -6,6 +6,7 @@
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import type { OidcClientMetaData } from '$lib/types/oidc.type'; import type { OidcClientMetaData } from '$lib/types/oidc.type';
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util'; import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import { mode } from 'mode-watcher';
const { const {
success, success,
@@ -28,6 +29,8 @@
animationDone = false; animationDone = false;
} }
}); });
const isLightMode = $derived(mode.current === 'light');
</script> </script>
<div class="flex justify-center gap-3"> <div class="flex justify-center gap-3">
@@ -60,8 +63,8 @@
</div> </div>
{:else if client.hasLogo} {:else if client.hasLogo}
<img <img
class="size-10 aspect-square object-contain" class="aspect-square size-10 object-contain"
src={cachedOidcClientLogo.getUrl(client.id)} src={cachedOidcClientLogo.getUrl(client.id, isLightMode)}
draggable={false} draggable={false}
alt={m.client_logo()} alt={m.client_logo()}
/> />

View File

@@ -23,6 +23,7 @@
'pt-BR': 'Português brasileiro', 'pt-BR': 'Português brasileiro',
ru: 'Русский', ru: 'Русский',
sv: 'Svenska', sv: 'Svenska',
tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
vi: 'Tiếng Việt', vi: 'Tiếng Việt',
'zh-CN': '简体中文', 'zh-CN': '简体中文',

View File

@@ -40,23 +40,40 @@
} }
async function updateImages( async function updateImages(
logoLight: File | null, logoLight: File | undefined,
logoDark: File | null, logoDark: File | undefined,
backgroundImage: File | null, defaultProfilePicture: File | null | undefined,
favicon: File | null backgroundImage: File | undefined,
favicon: File | undefined
) { ) {
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve(); const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
const lightLogoPromise = logoLight const lightLogoPromise = logoLight
? appConfigService.updateLogo(logoLight, true) ? appConfigService.updateLogo(logoLight, true)
: Promise.resolve(); : Promise.resolve();
const darkLogoPromise = logoDark const darkLogoPromise = logoDark
? appConfigService.updateLogo(logoDark, false) ? appConfigService.updateLogo(logoDark, false)
: Promise.resolve(); : Promise.resolve();
const defaultProfilePicturePromise =
defaultProfilePicture === null
? appConfigService.deleteDefaultProfilePicture()
: defaultProfilePicture
? appConfigService.updateDefaultProfilePicture(defaultProfilePicture)
: Promise.resolve();
const backgroundImagePromise = backgroundImage const backgroundImagePromise = backgroundImage
? appConfigService.updateBackgroundImage(backgroundImage) ? appConfigService.updateBackgroundImage(backgroundImage)
: Promise.resolve(); : Promise.resolve();
await Promise.all([lightLogoPromise, darkLogoPromise, backgroundImagePromise, faviconPromise]) await Promise.all([
lightLogoPromise,
darkLogoPromise,
defaultProfilePicturePromise,
backgroundImagePromise,
faviconPromise
])
.then(() => toast.success(m.images_updated_successfully())) .then(() => toast.success(m.images_updated_successfully()))
.catch(axiosErrorToast); .catch(axiosErrorToast);
} }

View File

@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import FileInput from '$lib/components/form/file-input.svelte'; import FileInput from '$lib/components/form/file-input.svelte';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import { cn } from '$lib/utils/style'; import { cn } from '$lib/utils/style';
import { LucideUpload } from '@lucide/svelte'; import { LucideImageOff, LucideUpload, LucideX } from '@lucide/svelte';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
let { let {
@@ -13,54 +14,98 @@
imageURL, imageURL,
accept = 'image/png, image/jpeg, image/svg+xml, image/gif, image/webp, image/avif, image/heic', accept = 'image/png, image/jpeg, image/svg+xml, image/gif, image/webp, image/avif, image/heic',
forceColorScheme, forceColorScheme,
isResetable = false,
isImageSet = $bindable(true),
...restProps ...restProps
}: HTMLAttributes<HTMLDivElement> & { }: HTMLAttributes<HTMLDivElement> & {
id: string; id: string;
imageClass: string; imageClass: string;
label: string; label: string;
image: File | null; image: File | null | undefined;
imageURL: string; imageURL: string;
forceColorScheme?: 'light' | 'dark'; forceColorScheme?: 'light' | 'dark';
accept?: string; accept?: string;
isResetable?: boolean;
isImageSet?: boolean;
} = $props(); } = $props();
let imageDataURL = $state(imageURL); let imageDataURL = $state(imageURL);
$effect(() => {
if (image) {
isImageSet = true;
}
});
function onImageChange(e: Event) { function onImageChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0] || null; const file = (e.target as HTMLInputElement).files?.[0] || undefined;
if (!file) return; if (!file) return;
image = file; image = file;
imageDataURL = URL.createObjectURL(file); imageDataURL = URL.createObjectURL(file);
} }
function onReset() {
image = null;
imageDataURL = imageURL;
isImageSet = false;
}
</script> </script>
<div class="flex flex-col items-start md:flex-row md:items-center" {...restProps}> <div class="flex flex-col items-start md:flex-row md:items-center" {...restProps}>
<Label class="w-52" for={id}>{label}</Label> <Label class="w-52" for={id}>{label}</Label>
<FileInput {id} variant="secondary" {accept} onchange={onImageChange}> <FileInput {id} variant="secondary" {accept} onchange={onImageChange}>
<div <div
class={{ class={cn('group/image relative flex items-center rounded transition-colors', {
'group relative flex items-center rounded': true,
'bg-[#F5F5F5]': forceColorScheme === 'light', 'bg-[#F5F5F5]': forceColorScheme === 'light',
'bg-[#262626]': forceColorScheme === 'dark', 'bg-[#262626]': forceColorScheme === 'dark',
'bg-muted': !forceColorScheme 'bg-muted': !forceColorScheme
}} })}
> >
{#if !isImageSet}
<div
class={cn(
'flex h-full w-full items-center justify-center p-3 transition-opacity duration-200',
'group-hover/image:opacity-10 group-has-[button:hover]/image:opacity-100',
imageClass
)}
>
<LucideImageOff class="text-muted-foreground" />
</div>
{:else}
<img <img
class={cn( class={cn(
'h-full w-full rounded object-cover p-3 transition-opacity duration-200 group-hover:opacity-10', 'h-full w-full rounded object-cover p-3 transition-opacity duration-200',
'group-hover/image:opacity-10 group-has-[button:hover]/image:opacity-100',
imageClass imageClass
)} )}
src={imageDataURL} src={imageDataURL}
alt={label} alt={label}
onerror={() => (isImageSet = false)}
/> />
{/if}
<LucideUpload <LucideUpload
class={{ class={cn(
'absolute top-1/2 left-1/2 size-5 -translate-x-1/2 -translate-y-1/2 transform font-medium opacity-0 transition-opacity group-hover:opacity-100': true, 'absolute top-1/2 left-1/2 size-5 -translate-x-1/2 -translate-y-1/2 transform font-medium opacity-0 transition-opacity duration-200',
'group-hover/image:opacity-100 group-has-[button:hover]/image:opacity-0',
{
'text-black': forceColorScheme === 'light', 'text-black': forceColorScheme === 'light',
'text-white': forceColorScheme === 'dark' 'text-white': forceColorScheme === 'dark'
}} }
)}
/> />
{#if isResetable && isImageSet}
<Button
size="icon"
onclick={(e) => {
e.stopPropagation();
onReset();
}}
class="absolute -top-2 -right-2 size-6 rounded-full shadow-md"
>
<LucideX class="size-3" />
</Button>
{/if}
</div> </div>
</FileInput> </FileInput>
</div> </div>

View File

@@ -1,24 +1,32 @@
<script lang="ts"> <script lang="ts">
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-image-util'; import {
cachedApplicationLogo,
cachedBackgroundImage,
cachedDefaultProfilePicture
} from '$lib/utils/cached-image-util';
import ApplicationImage from './application-image.svelte'; import ApplicationImage from './application-image.svelte';
let { let {
callback callback
}: { }: {
callback: ( callback: (
logoLight: File | null, logoLight: File | undefined,
logoDark: File | null, logoDark: File | undefined,
backgroundImage: File | null, defaultProfilePicture: File | null | undefined,
favicon: File | null backgroundImage: File | undefined,
favicon: File | undefined
) => void; ) => void;
} = $props(); } = $props();
let logoLight = $state<File | null>(null); let logoLight = $state<File | undefined>();
let logoDark = $state<File | null>(null); let logoDark = $state<File | undefined>();
let backgroundImage = $state<File | null>(null); let defaultProfilePicture = $state<File | null | undefined>();
let favicon = $state<File | null>(null); let backgroundImage = $state<File | undefined>();
let favicon = $state<File | undefined>();
let defaultProfilePictureSet = $state(true);
</script> </script>
<div class="flex flex-col gap-8"> <div class="flex flex-col gap-8">
@@ -46,6 +54,15 @@
imageURL={cachedApplicationLogo.getUrl(false)} imageURL={cachedApplicationLogo.getUrl(false)}
forceColorScheme="dark" forceColorScheme="dark"
/> />
<ApplicationImage
id="default-profile-picture"
imageClass="size-24"
label={m.default_profile_picture()}
isResetable
bind:image={defaultProfilePicture}
imageURL={cachedDefaultProfilePicture.getUrl()}
isImageSet={defaultProfilePictureSet}
/>
<ApplicationImage <ApplicationImage
id="background-image" id="background-image"
imageClass="h-[350px] max-w-[500px]" imageClass="h-[350px] max-w-[500px]"
@@ -55,7 +72,10 @@
/> />
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<Button class="mt-5" onclick={() => callback(logoLight, logoDark, backgroundImage, favicon)} <Button
class="mt-5"
usePromiseLoading
onclick={() => callback(logoLight, logoDark, defaultProfilePicture, backgroundImage, favicon)}
>{m.save()}</Button >{m.save()}</Button
> >
</div> </div>

View File

@@ -21,9 +21,15 @@
async function createOIDCClient(client: OidcClientCreateWithLogo) { async function createOIDCClient(client: OidcClientCreateWithLogo) {
try { try {
const createdClient = await oidcService.createClient(client); const createdClient = await oidcService.createClient(client);
if (client.logo) {
await oidcService.updateClientLogo(createdClient, client.logo); const logoPromise = client.logo
} ? oidcService.updateClientLogo(createdClient, client.logo, true)
: Promise.resolve();
const darkLogoPromise = client.darkLogo
? oidcService.updateClientLogo(createdClient, client.darkLogo, false)
: Promise.resolve();
await Promise.all([logoPromise, darkLogoPromise]);
const clientSecret = await oidcService.createClientSecret(createdClient.id); const clientSecret = await oidcService.createClientSecret(createdClient.id);
clientSecretStore.set(clientSecret); clientSecretStore.set(clientSecret);
goto(`/settings/admin/oidc-clients/${createdClient.id}`); goto(`/settings/admin/oidc-clients/${createdClient.id}`);

View File

@@ -70,6 +70,7 @@
const actions: CreateAdvancedTableActions<OidcClientWithAllowedUserGroupsCount> = (_) => [ const actions: CreateAdvancedTableActions<OidcClientWithAllowedUserGroupsCount> = (_) => [
{ {
label: m.edit(), label: m.edit(),
primary: true,
icon: LucidePencil, icon: LucidePencil,
onClick: (client) => goto(`/settings/admin/oidc-clients/${client.id}`) onClick: (client) => goto(`/settings/admin/oidc-clients/${client.id}`)
}, },

View File

@@ -41,6 +41,7 @@
const actions: CreateAdvancedTableActions<UserGroupWithUserCount> = (group) => [ const actions: CreateAdvancedTableActions<UserGroupWithUserCount> = (group) => [
{ {
label: m.edit(), label: m.edit(),
primary: true,
icon: LucidePencil, icon: LucidePencil,
variant: 'ghost', variant: 'ghost',
onClick: (group) => goto(`/settings/admin/user-groups/${group.id}`) onClick: (group) => goto(`/settings/admin/user-groups/${group.id}`)

View File

@@ -143,6 +143,7 @@
}, },
{ {
label: m.edit(), label: m.edit(),
primary: true,
icon: LucidePencil, icon: LucidePencil,
onClick: (u) => goto(`/settings/admin/users/${u.id}`) onClick: (u) => goto(`/settings/admin/users/${u.id}`)
}, },

View File

@@ -12,5 +12,5 @@
"test": "pnpm --filter pocket-id-tests test", "test": "pnpm --filter pocket-id-tests test",
"format": "pnpm --filter pocket-id-frontend format" "format": "pnpm --filter pocket-id-frontend format"
}, },
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
} }

View File

@@ -69,6 +69,10 @@ else
# FreeBSD builds # FreeBSD builds
build_platform "freebsd-amd64" "freebsd" "amd64" "" build_platform "freebsd-amd64" "freebsd" "amd64" ""
build_platform "freebsd-arm64" "freebsd" "arm64" "" build_platform "freebsd-arm64" "freebsd" "arm64" ""
# OpenBSD builds
build_platform "openbsd-amd64" "openbsd" "amd64" ""
build_platform "openbsd-arm64" "openbsd" "arm64" ""
fi fi
echo "Compilation done" echo "Compilation done"

View File

@@ -0,0 +1,42 @@
services:
lldap:
extends:
file: docker-compose.yml
service: lldap
localstack-s3:
image: localstack/localstack:s3-latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localstack-s3:4566"]
interval: 1s
timeout: 3s
retries: 10
create-bucket:
image: amazon/aws-cli
environment:
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
depends_on:
localstack-s3:
condition: service_healthy
entrypoint: "aws --endpoint-url=http://localstack-s3:4566 s3 mb s3://pocket-id-test"
pocket-id:
extends:
file: docker-compose.yml
service: pocket-id
environment:
FILE_BACKEND: s3
S3_BUCKET: pocket-id-test
S3_REGION: us-east-1
S3_ENDPOINT: http://localstack-s3:4566
S3_ACCESS_KEY_ID: test
S3_SECRET_ACCESS_KEY: test
S3_FORCE_PATH_STYLE: true
KEYS_STORAGE: database
ENCRYPTION_KEY: test
depends_on:
create-bucket:
condition: service_completed_successfully

View File

@@ -122,10 +122,13 @@ test('Update application images', async ({ page }) => {
await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico'); await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico');
await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png'); await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png');
await page.getByLabel('Dark Mode Logo').setInputFiles('assets/nextcloud-logo.png'); await page.getByLabel('Dark Mode Logo').setInputFiles('assets/nextcloud-logo.png');
await page.getByLabel('Default Profile Picture').setInputFiles('assets/pingvin-share-logo.png');
await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg'); await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg');
await page.getByRole('button', { name: 'Save' }).last().click(); await page.getByRole('button', { name: 'Save' }).last().click();
await expect(page.locator('[data-type="success"]')).toHaveText('Images updated successfully'); await expect(page.locator('[data-type="success"]')).toHaveText(
'Images updated successfully. It may take a few minutes to update.'
);
await page.request await page.request
.get('/api/application-images/favicon') .get('/api/application-images/favicon')