mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-11 07:32:57 +03:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b086cebcd | ||
|
|
1f3550c9bd | ||
|
|
912008b048 | ||
|
|
5ad8b03831 | ||
|
|
c1e515a05f | ||
|
|
654593b4b6 | ||
|
|
8999173aa0 | ||
|
|
10b087640f | ||
|
|
d0392d25ed | ||
|
|
2ffc6ba42a | ||
|
|
c114a2edaa | ||
|
|
63db4d5120 | ||
|
|
d8c73ed472 | ||
|
|
5971bfbfa6 | ||
|
|
29eacd6424 | ||
|
|
21ca87be38 | ||
|
|
1283314f77 | ||
|
|
9c54e2e6b0 | ||
|
|
a5efb95065 | ||
|
|
625f235740 | ||
|
|
2c122d413d | ||
|
|
fc0c99a232 | ||
|
|
24e274200f | ||
|
|
0aab3f3c7a | ||
|
|
182d809028 | ||
|
|
c51265dafb | ||
|
|
0cb039d35d | ||
|
|
7ab0fd3028 | ||
|
|
49f0fa423c | ||
|
|
61e63e411d | ||
|
|
9339e88a5a | ||
|
|
fe003b927c | ||
|
|
f5b5b1bd85 | ||
|
|
d28bfac81f | ||
|
|
b04e3e8ecf | ||
|
|
d77d8eb068 | ||
|
|
7cd88aca25 | ||
|
|
b5e6371eaa | ||
|
|
544b98c1d0 | ||
|
|
3188e92257 | ||
|
|
3fa2f9a162 | ||
|
|
7b1f6b8857 | ||
|
|
17d8893bdb | ||
|
|
0e44f245af | ||
|
|
824e8f1a0f | ||
|
|
6e4d2a4a33 | ||
|
|
6c65bd34cd | ||
|
|
7bfe4834d0 | ||
|
|
484c2f6ef2 | ||
|
|
87956ea725 | ||
|
|
32dd403038 | ||
|
|
4d59e72866 | ||
|
|
9ac5d51187 | ||
|
|
5a031f5d1b | ||
|
|
535bc9f46b | ||
|
|
f0c144c51c | ||
|
|
61e4ea45fb | ||
|
|
06e1656923 | ||
|
|
0a3b1c6530 | ||
|
|
d479817b6a | ||
|
|
01bf31d23d | ||
|
|
42a861d206 | ||
|
|
78266e3e4c | ||
|
|
c8478d75be | ||
|
|
28d93b00a3 | ||
|
|
12a7a6a5c5 | ||
|
|
a6d5071724 | ||
|
|
cebe2242b9 | ||
|
|
56ee7d946f | ||
|
|
f3c6521f2b | ||
|
|
ffed465f09 | ||
|
|
c359b5be06 | ||
|
|
e9a023bb71 | ||
|
|
60f0b28076 | ||
|
|
d541c9ab4a | ||
|
|
024ed53022 | ||
|
|
2c78bd1b46 | ||
|
|
5602d79611 | ||
|
|
51b73c9c31 | ||
|
|
10f0580a43 | ||
|
|
a1488565ea | ||
|
|
35d5f887ce | ||
|
|
4c76de45ed |
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/bug.yml
vendored
23
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -2,7 +2,6 @@ name: "🐛 Bug Report"
|
||||
description: "Report something that is not working as expected"
|
||||
title: "🐛 Bug Report: "
|
||||
type: 'Bug'
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -37,13 +36,29 @@ body:
|
||||
value: |
|
||||
### Additional Information
|
||||
- type: textarea
|
||||
id: extra-information
|
||||
id: version
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "Version and Environment"
|
||||
description: "Please specify the version of Pocket ID, along with any environment-specific configurations, such your reverse proxy, that might be relevant."
|
||||
label: "Pocket ID Version"
|
||||
description: "Please specify the version of Pocket ID."
|
||||
placeholder: "e.g., v0.24.1"
|
||||
- type: textarea
|
||||
id: database
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "Database"
|
||||
description: "Please specify the database in use: SQLite or Postgres (including version)."
|
||||
placeholder: "e.g., SQLite or Postgres 17"
|
||||
- type: textarea
|
||||
id: environment
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "OS and Environment"
|
||||
description: "Please include the OS, whether you're using containers (Docker, Podman, etc) along with any environment-specific configurations, such your reverse proxy, that might be relevant."
|
||||
placeholder: "e.g., Docker on Ubuntu 24.04, served using Traefik"
|
||||
- type: textarea
|
||||
id: log-files
|
||||
validations:
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature.yml
vendored
1
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: 🚀 Feature
|
||||
description: "Submit a proposal for a new feature"
|
||||
title: "🚀 Feature: "
|
||||
labels: [feature]
|
||||
type: 'Feature'
|
||||
body:
|
||||
- type: textarea
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/language-request.yml
vendored
1
.github/ISSUE_TEMPLATE/language-request.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: "🌐 Language request"
|
||||
description: "You want to contribute to a language that isn't on Crowdin yet?"
|
||||
title: "🌐 Language Request: <language name in english>"
|
||||
labels: [language-request]
|
||||
type: 'Language Request'
|
||||
body:
|
||||
- type: input
|
||||
|
||||
4
.github/workflows/backend-linter.yml
vendored
4
.github/workflows/backend-linter.yml
vendored
@@ -32,9 +32,9 @@ jobs:
|
||||
go-version-file: backend/go.mod
|
||||
|
||||
- name: Run Golangci-lint
|
||||
uses: golangci/golangci-lint-action@dec74fa03096ff515422f71d18d41307cacde373 # v7.0.0
|
||||
uses: golangci/golangci-lint-action@v8.0.0
|
||||
with:
|
||||
version: v2.0.2
|
||||
version: v2.4.0
|
||||
args: --build-tags=exclude_frontend
|
||||
working-directory: backend
|
||||
only-new-issues: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
20
.github/workflows/build-next.yml
vendored
20
.github/workflows/build-next.yml
vendored
@@ -21,17 +21,22 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "backend/go.mod"
|
||||
go-version-file: 'backend/go.mod'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -54,12 +59,11 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build binaries
|
||||
run: sh scripts/development/build-binaries.sh --docker-only
|
||||
@@ -85,12 +89,12 @@ jobs:
|
||||
- name: Container image attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
||||
subject-digest: ${{ steps.build-push-image.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Container image attestation (distroless)
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
||||
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
175
.github/workflows/e2e-tests.yml
vendored
175
.github/workflows/e2e-tests.yml
vendored
@@ -45,39 +45,54 @@ jobs:
|
||||
path: /tmp/docker-image.tar
|
||||
retention-days: 1
|
||||
|
||||
test-sqlite:
|
||||
test:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
db: [sqlite, postgres]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Download Docker image artifact
|
||||
uses: actions/download-artifact@v4
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Cache PostgreSQL Docker image
|
||||
if: matrix.db == 'postgres'
|
||||
uses: actions/cache@v3
|
||||
id: postgres-cache
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp
|
||||
path: /tmp/postgres-image.tar
|
||||
key: postgres-17-${{ runner.os }}
|
||||
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/docker-image.tar
|
||||
- name: Pull and save PostgreSQL image
|
||||
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull postgres:17
|
||||
docker save postgres:17 > /tmp/postgres-image.tar
|
||||
|
||||
- name: Load PostgreSQL image from cache
|
||||
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/postgres-image.tar
|
||||
- name: Cache LLDAP Docker image
|
||||
uses: actions/cache@v3
|
||||
id: lldap-cache
|
||||
@@ -94,31 +109,45 @@ jobs:
|
||||
- name: Load LLDAP image from cache
|
||||
if: steps.lldap-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/lldap-image.tar
|
||||
- name: Download Docker image artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/docker-image.tar
|
||||
|
||||
- name: Install test dependencies
|
||||
working-directory: ./tests
|
||||
run: npm ci
|
||||
run: pnpm --filter pocket-id-tests install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
working-directory: ./tests
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Docker Container with Sqlite DB and LDAP
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Run Docker Container (sqlite) with LDAP
|
||||
if: matrix.db == 'sqlite'
|
||||
working-directory: ./tests/setup
|
||||
run: |
|
||||
docker compose up -d
|
||||
docker compose logs -f pocket-id &> /tmp/backend.log &
|
||||
|
||||
- name: Run Docker Container (postgres) with LDAP
|
||||
if: matrix.db == 'postgres'
|
||||
working-directory: ./tests/setup
|
||||
run: |
|
||||
docker compose -f docker-compose-postgres.yml up -d
|
||||
docker compose -f docker-compose-postgres.yml logs -f pocket-id &> /tmp/backend.log &
|
||||
|
||||
- name: Run Playwright tests
|
||||
working-directory: ./tests
|
||||
run: npx playwright test
|
||||
run: pnpm exec playwright test
|
||||
|
||||
- name: Upload Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: playwright-report-sqlite
|
||||
name: playwright-report-${{ matrix.db }}
|
||||
path: tests/.report
|
||||
include-hidden-files: true
|
||||
retention-days: 15
|
||||
@@ -127,111 +156,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: backend-sqlite
|
||||
path: /tmp/backend.log
|
||||
include-hidden-files: true
|
||||
retention-days: 15
|
||||
|
||||
test-postgres:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Cache PostgreSQL Docker image
|
||||
uses: actions/cache@v3
|
||||
id: postgres-cache
|
||||
with:
|
||||
path: /tmp/postgres-image.tar
|
||||
key: postgres-17-${{ runner.os }}
|
||||
|
||||
- name: Pull and save PostgreSQL image
|
||||
if: steps.postgres-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull postgres:17
|
||||
docker save postgres:17 > /tmp/postgres-image.tar
|
||||
|
||||
- name: Load PostgreSQL image from cache
|
||||
if: steps.postgres-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/postgres-image.tar
|
||||
|
||||
- name: Cache LLDAP Docker image
|
||||
uses: actions/cache@v3
|
||||
id: lldap-cache
|
||||
with:
|
||||
path: /tmp/lldap-image.tar
|
||||
key: lldap-stable-${{ runner.os }}
|
||||
|
||||
- name: Pull and save LLDAP image
|
||||
if: steps.lldap-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull nitnelave/lldap:stable
|
||||
docker save nitnelave/lldap:stable > /tmp/lldap-image.tar
|
||||
|
||||
- name: Load LLDAP image from cache
|
||||
if: steps.lldap-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/lldap-image.tar
|
||||
|
||||
- name: Download Docker image artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/docker-image.tar
|
||||
|
||||
- name: Install test dependencies
|
||||
working-directory: ./tests
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
working-directory: ./tests
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Docker Container with Postgres DB and LDAP
|
||||
working-directory: ./tests/setup
|
||||
run: |
|
||||
docker compose -f docker-compose-postgres.yml up -d
|
||||
docker compose -f docker-compose-postgres.yml logs -f pocket-id &> /tmp/backend.log &
|
||||
|
||||
- name: Run Playwright tests
|
||||
working-directory: ./tests
|
||||
run: npx playwright test
|
||||
|
||||
- name: Upload Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: playwright-report-postgres
|
||||
path: frontend/tests/.report
|
||||
include-hidden-files: true
|
||||
retention-days: 15
|
||||
|
||||
- name: Upload Backend Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: backend-postgres
|
||||
name: backend-${{ matrix.db }}
|
||||
path: /tmp/backend.log
|
||||
include-hidden-files: true
|
||||
retention-days: 15
|
||||
|
||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -3,7 +3,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,15 +16,19 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "backend/go.mod"
|
||||
go-version-file: 'backend/go.mod'
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@@ -62,13 +66,11 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
- name: Build frontend
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
run: pnpm --filter pocket-id-frontend build
|
||||
- name: Build binaries
|
||||
run: sh scripts/development/build-binaries.sh
|
||||
- name: Build and push container image
|
||||
@@ -94,17 +96,17 @@ jobs:
|
||||
- name: Binary attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: "backend/.bin/pocket-id-**"
|
||||
subject-path: 'backend/.bin/pocket-id-**'
|
||||
- name: Container image attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
||||
subject-digest: ${{ steps.container-build-push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Container image attestation (distroless)
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
||||
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Upload binaries to release
|
||||
@@ -121,6 +123,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Mark release as published
|
||||
run: gh release edit ${{ github.ref_name }} --draft=false
|
||||
|
||||
40
.github/workflows/svelte-check.yml
vendored
40
.github/workflows/svelte-check.yml
vendored
@@ -4,21 +4,21 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "frontend/src/**"
|
||||
- ".github/svelte-check-matcher.json"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/tsconfig.json"
|
||||
- "frontend/svelte.config.js"
|
||||
- 'frontend/src/**'
|
||||
- '.github/svelte-check-matcher.json'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- 'frontend/tsconfig.json'
|
||||
- 'frontend/svelte.config.js'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "frontend/src/**"
|
||||
- ".github/svelte-check-matcher.json"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/tsconfig.json"
|
||||
- "frontend/svelte.config.js"
|
||||
- 'frontend/src/**'
|
||||
- '.github/svelte-check-matcher.json'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- 'frontend/tsconfig.json'
|
||||
- 'frontend/svelte.config.js'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -36,24 +36,28 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
|
||||
- name: Build Pocket ID Frontend
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
run: pnpm --filter pocket-id-frontend build
|
||||
|
||||
- name: Add svelte-check problem matcher
|
||||
run: echo "::add-matcher::.github/svelte-check-matcher.json"
|
||||
|
||||
- name: Run svelte-check
|
||||
working-directory: frontend
|
||||
run: npm run check
|
||||
run: pnpm --filter pocket-id-frontend check
|
||||
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,3 +1,75 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.8.1...v) (2025-08-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support automatic db migration rollbacks ([#874](https://github.com/pocket-id/pocket-id/issues/874)) ([c114a2e](https://github.com/pocket-id/pocket-id/commit/c114a2edaae4c007c75c34c02e8b0bb011845cae))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't force uuid for client id in postgres ([2ffc6ba](https://github.com/pocket-id/pocket-id/commit/2ffc6ba42af4742a13b77543142b66b3e826ab88))
|
||||
* ensure SQLite has a writable temporary directory ([#876](https://github.com/pocket-id/pocket-id/issues/876)) ([1f3550c](https://github.com/pocket-id/pocket-id/commit/1f3550c9bd3aafd3bd2272ef47f3ed8736037d81))
|
||||
* sort order incorrect for apps when using postgres ([d0392d2](https://github.com/pocket-id/pocket-id/commit/d0392d25edcaa5f3c7da2aad70febf63b47763fa))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.8.0...v) (2025-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* migration clears allowed users groups ([5971bfb](https://github.com/pocket-id/pocket-id/commit/5971bfbfa66ecfebf2b1c08d34fcbd8c18cdc046))
|
||||
* wrong column type for reauthentication tokens in Postgres ([#869](https://github.com/pocket-id/pocket-id/issues/869)) ([1283314](https://github.com/pocket-id/pocket-id/commit/1283314f776a0ba43be7d796e7e2243e31f860de))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.7.0...v) (2025-08-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option to OIDC client to require re-authentication ([#747](https://github.com/pocket-id/pocket-id/issues/747)) ([0cb039d](https://github.com/pocket-id/pocket-id/commit/0cb039d35d49206011064e622f3bfd3d8f88720f))
|
||||
* allow custom client IDs ([#864](https://github.com/pocket-id/pocket-id/issues/864)) ([a5efb95](https://github.com/pocket-id/pocket-id/commit/a5efb9506582884c70b9b1fd737ebdd44b101b47))
|
||||
* display all accessible oidc clients in the dashboard ([#832](https://github.com/pocket-id/pocket-id/issues/832)) ([3188e92](https://github.com/pocket-id/pocket-id/commit/3188e92257afcaf7a16dd418e4c40626d7e1d034))
|
||||
* login code font change ([#851](https://github.com/pocket-id/pocket-id/issues/851)) ([d28bfac](https://github.com/pocket-id/pocket-id/commit/d28bfac81fc24ee79e4896538a616f0a89ab30a5))
|
||||
* **signup:** add default user groups and claims for new users ([#812](https://github.com/pocket-id/pocket-id/issues/812)) ([182d809](https://github.com/pocket-id/pocket-id/commit/182d8090286f9953171c6c410283be679889aca7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* authorization can't be revoked ([0aab3f3](https://github.com/pocket-id/pocket-id/commit/0aab3f3c7ad8c1b14939de3ded60c9f201eab8fc))
|
||||
* delete webauthn session after login to prevent replay attacks ([fe003b9](https://github.com/pocket-id/pocket-id/commit/fe003b927ce7772692439992860c804de89ce424))
|
||||
* **deps:** bump rollup from 4.45.3 to 4.46.3 ([#845](https://github.com/pocket-id/pocket-id/issues/845)) ([b5e6371](https://github.com/pocket-id/pocket-id/commit/b5e6371eaaf3d9e85d8b05c457487c4425fa8381))
|
||||
* enable foreign key check for sqlite ([#863](https://github.com/pocket-id/pocket-id/issues/863)) ([625f235](https://github.com/pocket-id/pocket-id/commit/625f23574001ebd7074b8d98d448a2811847be16))
|
||||
* ferated identities can't be cleared ([24e2742](https://github.com/pocket-id/pocket-id/commit/24e274200fe4002d01c58cc3fa74094b598d7599))
|
||||
* for one-time access tokens and signup tokens, pass TTLs instead of absolute expiration date ([#855](https://github.com/pocket-id/pocket-id/issues/855)) ([7ab0fd3](https://github.com/pocket-id/pocket-id/commit/7ab0fd30286e6b67b5ce586484d82a20c42b471d))
|
||||
* ignore client secret if client is public ([#836](https://github.com/pocket-id/pocket-id/issues/836)) ([7b1f6b8](https://github.com/pocket-id/pocket-id/commit/7b1f6b88572bac1f3e838a9e904917fbd5fbdf61))
|
||||
* move audit log call before TX is committed ([#854](https://github.com/pocket-id/pocket-id/issues/854)) ([9339e88](https://github.com/pocket-id/pocket-id/commit/9339e88a5a26ff77a5e40149cbb1a5b339b7ec6a))
|
||||
* non admin users can't revoke oidc client but see edit link ([0e44f24](https://github.com/pocket-id/pocket-id/commit/0e44f245afcdf8179bf619613ca9ef4bffa176ca))
|
||||
* oidc client advanced options color ([fc0c99a](https://github.com/pocket-id/pocket-id/commit/fc0c99a232b0efb1a5b5d2c551102418b1080293))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.6.4...v) (2025-08-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add robots.txt to block indexing ([#806](https://github.com/pocket-id/pocket-id/issues/806)) ([06e1656](https://github.com/pocket-id/pocket-id/commit/06e1656923eb2f4531be497716f9147c09d60b65))
|
||||
* add support for `code_challenge_methods_supported` ([#794](https://github.com/pocket-id/pocket-id/issues/794)) ([d479817](https://github.com/pocket-id/pocket-id/commit/d479817b6a7ca4807b5de500b3ba713d436b0770))
|
||||
* Support OTel and JSON for logs (via log/slog) ([#760](https://github.com/pocket-id/pocket-id/issues/760)) ([78266e3](https://github.com/pocket-id/pocket-id/commit/78266e3e4cab2b23249c3baf20f4387d00eebd9e))
|
||||
* support reading secret env vars from _FILE ([#799](https://github.com/pocket-id/pocket-id/issues/799)) ([0a3b1c6](https://github.com/pocket-id/pocket-id/commit/0a3b1c653050f2237d30ec437c5de88baa704a25))
|
||||
* user application dashboard ([#727](https://github.com/pocket-id/pocket-id/issues/727)) ([484c2f6](https://github.com/pocket-id/pocket-id/commit/484c2f6ef20efc1fade1a41e2aeace54c7bb4f1b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* admins can not delete or disable their own account ([f0c144c](https://github.com/pocket-id/pocket-id/commit/f0c144c51c635bc348222a00d3bc88bc4e0711ef))
|
||||
* authorization animation not working ([9ac5d51](https://github.com/pocket-id/pocket-id/commit/9ac5d5118710cad59c8c4ce7cef7ab09be3de664))
|
||||
* custom claims input suggestions instantly close after opening ([4d59e72](https://github.com/pocket-id/pocket-id/commit/4d59e7286666480e20c728787a95e82513509240))
|
||||
* delete WebAuthn registration session after use ([#783](https://github.com/pocket-id/pocket-id/issues/783)) ([c8478d7](https://github.com/pocket-id/pocket-id/commit/c8478d75bed7295625cd3cf62ef46fcd95902410))
|
||||
* set input type 'email' for email-based login ([#776](https://github.com/pocket-id/pocket-id/issues/776)) ([d541c9a](https://github.com/pocket-id/pocket-id/commit/d541c9ab4af8d7283891a80f886dd5d4ebc52f53))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.6.3...v) (2025-07-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* migration fails on postgres ([#762](https://github.com/pocket-id/pocket-id/issues/762)) ([35d5f88](https://github.com/pocket-id/pocket-id/commit/35d5f887ce7c88933d7e4c2f0acd2aeedd18c214))
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.6.2...v) (2025-07-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -28,7 +28,7 @@ Before you submit the pull request for review please ensure that
|
||||
- **refactor** - code change that neither fixes a bug nor adds a feature
|
||||
|
||||
- Your pull request has a detailed description
|
||||
- You run `npm run format` to format the code
|
||||
- You run `pnpm format` to format the code
|
||||
|
||||
## Development Environment
|
||||
|
||||
@@ -52,7 +52,7 @@ If you use [Dev Containers](https://code.visualstudio.com/docs/remote/containers
|
||||
If you don't use Dev Containers, you need to install the following tools manually:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 22
|
||||
- [Go](https://golang.org/doc/install) >= 1.24
|
||||
- [Go](https://golang.org/doc/install) >= 1.25
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
### 2. Setup
|
||||
@@ -69,10 +69,10 @@ The backend is built with [Gin](https://gin-gonic.com) and written in Go. To set
|
||||
|
||||
The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in TypeScript. To set it up, follow these steps:
|
||||
|
||||
1. Open the `frontend` folder
|
||||
2. Copy the `.env.development-example` file to `.env` and edit the variables as needed
|
||||
3. Install the dependencies with `npm install`
|
||||
4. Start the frontend with `npm run dev`
|
||||
1. Open the `pocket-id` project folder
|
||||
2. Copy the `frontend/.env.development-example` file to `frontend/.env` and edit the variables as needed
|
||||
3. Install the dependencies with `pnpm install`
|
||||
4. Start the frontend with `pnpm dev`
|
||||
|
||||
You're all set! The application is now listening on `localhost:3000`. The backend gets proxied trough the frontend in development mode.
|
||||
|
||||
@@ -84,11 +84,13 @@ If you are contributing to a new feature please ensure that you add tests for it
|
||||
|
||||
The tests can be run like this:
|
||||
|
||||
1. Visit the setup folder by running `cd tests/setup`
|
||||
1. Install the dependencies from the root of the project `pnpm install`
|
||||
|
||||
2. Start the test environment by running `docker compose up -d --build`
|
||||
2. Visit the setup folder by running `cd tests/setup`
|
||||
|
||||
3. Go back to the test folder by running `cd ..`
|
||||
4. Run the tests with `npx playwright test`
|
||||
3. Start the test environment by running `docker compose up -d --build`
|
||||
|
||||
4. Go back to the test folder by running `cd ..`
|
||||
5. Run the tests with `pnpm dlx playwright test` or from the root project folder `pnpm test`
|
||||
|
||||
If you make any changes to the application, you have to rebuild the test environment by running `docker compose up -d --build` again.
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -5,21 +5,27 @@ ARG BUILD_TAGS=""
|
||||
|
||||
# Stage 1: Build Frontend
|
||||
FROM node:22-alpine AS frontend-builder
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
COPY ./frontend/package*.json ./
|
||||
RUN npm ci
|
||||
COPY ./frontend ./
|
||||
RUN BUILD_OUTPUT_PATH=dist npm run build
|
||||
|
||||
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY frontend/package.json ./frontend/
|
||||
RUN pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
|
||||
COPY ./frontend ./frontend/
|
||||
|
||||
RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
|
||||
|
||||
# Stage 2: Build Backend
|
||||
FROM golang:1.24-alpine AS backend-builder
|
||||
FROM golang:1.25-alpine AS backend-builder
|
||||
ARG BUILD_TAGS
|
||||
WORKDIR /build
|
||||
COPY ./backend/go.mod ./backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY ./backend ./
|
||||
COPY --from=frontend-builder /build/dist ./frontend/dist
|
||||
COPY --from=frontend-builder /build/frontend/dist ./frontend/dist
|
||||
COPY .version .version
|
||||
|
||||
WORKDIR /build/cmd
|
||||
@@ -30,7 +36,7 @@ RUN VERSION=$(cat /build/.version) \
|
||||
-tags "${BUILD_TAGS}" \
|
||||
-ldflags="-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${VERSION} -buildid=${VERSION}" \
|
||||
-trimpath \
|
||||
-o /build/pocket-id-backend \
|
||||
-o /build/pocket-id \
|
||||
.
|
||||
|
||||
# Stage 3: Production Image
|
||||
@@ -39,7 +45,7 @@ WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl su-exec
|
||||
|
||||
COPY --from=backend-builder /build/pocket-id-backend /app/pocket-id
|
||||
COPY --from=backend-builder /build/pocket-id /app/pocket-id
|
||||
COPY ./scripts/docker /app/docker
|
||||
|
||||
RUN chmod +x /app/pocket-id && \
|
||||
|
||||
@@ -1,99 +1,108 @@
|
||||
module github.com/pocket-id/pocket-id/backend
|
||||
|
||||
go 1.24.0
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/cenkalti/backoff/v5 v5.0.2
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||
github.com/emersion/go-smtp v0.21.3
|
||||
github.com/fxamacker/cbor/v2 v2.7.0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/glebarez/go-sqlite v1.21.2
|
||||
github.com/fxamacker/cbor/v2 v2.9.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-co-op/gocron/v2 v2.15.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.3
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/go-playground/validator/v10 v10.25.0
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/golang-migrate/migrate/v4 v4.18.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.1
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10
|
||||
github.com/lmittmann/tint v1.1.2
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
|
||||
github.com/samber/slog-gin v1.15.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/log v0.13.0
|
||||
go.opentelemetry.io/otel/metric v1.37.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.10.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/image v0.24.0
|
||||
golang.org/x/text v0.26.0
|
||||
golang.org/x/time v0.9.0
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.25.12
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/text v0.28.0
|
||||
golang.org/x/time v0.12.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.10 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.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/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/disintegration/gift v1.1.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-webauthn/x v0.1.16 // indirect
|
||||
github.com/go-webauthn/x v0.1.23 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/google/go-github/v39 v39.2.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
@@ -104,7 +113,8 @@ require (
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
|
||||
@@ -119,22 +129,21 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.10.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/arch v0.14.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
)
|
||||
|
||||
225
backend/go.sum
225
backend/go.sum
@@ -8,30 +8,28 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI=
|
||||
github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
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/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
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.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
||||
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
||||
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
|
||||
github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
|
||||
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
||||
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||
@@ -54,27 +52,27 @@ github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGV
|
||||
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
|
||||
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
|
||||
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-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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -83,27 +81,35 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
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-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/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8=
|
||||
github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us=
|
||||
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/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||
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-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/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
|
||||
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
@@ -127,8 +133,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
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/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -157,10 +163,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -169,18 +173,22 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
|
||||
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.1 h1:fH3T748FCMbXoF9UXXNS9i0q6PpYyJZK/rKSbkt2guY=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.1/go.mod h1:XP2WqxMOSzHSyf3pfibCcfsLqbomxakAnNqiuaH8nwo=
|
||||
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/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/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
||||
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
@@ -208,10 +216,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2 h1:jG+FaCBv3h6GD5F+oenTfe3+0NmX8sCKjni5k3A5Dek=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
|
||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -231,6 +241,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/slog-gin v1.15.1 h1:jsnfr+S5HQPlz9pFPA3tOmKW7wN/znyZiE6hncucrTM=
|
||||
github.com/samber/slog-gin v1.15.1/go.mod h1:mPAEinK/g2jPLauuWO11m3Q0Ca7aG4k9XjXjXY8IhMQ=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
@@ -240,7 +252,6 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -248,18 +259,21 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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/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/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/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA=
|
||||
@@ -268,8 +282,8 @@ go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
|
||||
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/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/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/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
|
||||
@@ -292,47 +306,49 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||
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/log v0.10.0 h1:1CXmspaRITvFcjA4kyVszuG4HjA61fPDxMb7q3BuyF0=
|
||||
go.opentelemetry.io/otel/log v0.10.0/go.mod h1:PbVdm9bXKku/gL0oFfUF4wwsQsOPlpo4VEqjvxih+FM=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
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/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/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
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/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
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/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
|
||||
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
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/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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
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/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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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/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=
|
||||
@@ -344,8 +360,11 @@ 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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
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/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=
|
||||
@@ -353,8 +372,8 @@ 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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -367,8 +386,8 @@ 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -379,6 +398,7 @@ 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
@@ -387,27 +407,29 @@ 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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
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/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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||
google.golang.org/genproto/googleapis/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/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -415,20 +437,22 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -437,10 +461,9 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -12,17 +12,17 @@ import (
|
||||
)
|
||||
|
||||
// initApplicationImages copies the images from the images directory to the application-images directory
|
||||
func initApplicationImages() {
|
||||
func initApplicationImages() error {
|
||||
dirPath := common.EnvConfig.UploadPath + "/application-images"
|
||||
|
||||
sourceFiles, err := resources.FS.ReadDir("images")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Fatalf("Error reading directory: %v", err)
|
||||
return fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
destinationFiles, err := os.ReadDir(dirPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Fatalf("Error reading directory: %v", err)
|
||||
return fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
// Copy images from the images directory to the application-images directory if they don't already exist
|
||||
@@ -35,9 +35,11 @@ func initApplicationImages() {
|
||||
|
||||
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error copying file: %v", err)
|
||||
return fmt.Errorf("failed to copy file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
|
||||
|
||||
@@ -3,7 +3,7 @@ package bootstrap
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
@@ -14,16 +14,23 @@ import (
|
||||
)
|
||||
|
||||
func Bootstrap(ctx context.Context) error {
|
||||
initApplicationImages()
|
||||
|
||||
// Initialize the tracer and metrics exporter
|
||||
shutdownFns, httpClient, err := initOtel(ctx, common.EnvConfig.MetricsEnabled, common.EnvConfig.TracingEnabled)
|
||||
// Initialize the observability stack, including the logger, distributed tracing, and metrics
|
||||
shutdownFns, httpClient, err := initObservability(ctx, common.EnvConfig.MetricsEnabled, common.EnvConfig.TracingEnabled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize OpenTelemetry: %w", err)
|
||||
}
|
||||
slog.InfoContext(ctx, "Pocket ID is starting")
|
||||
|
||||
err = initApplicationImages()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize application images: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the database
|
||||
db := NewDatabase()
|
||||
db, err := NewDatabase()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
// Create all services
|
||||
svc, err := initServices(ctx, db, httpClient)
|
||||
@@ -62,7 +69,7 @@ func Bootstrap(ctx context.Context) error {
|
||||
NewServiceRunner(shutdownFns...).
|
||||
Run(shutdownCtx) //nolint:contextcheck
|
||||
if err != nil {
|
||||
log.Printf("Error shutting down services: %v", err)
|
||||
slog.Error("Error shutting down services", slog.Any("error", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,9 +3,10 @@ package bootstrap
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,24 +15,27 @@ import (
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/github"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
slogGorm "github.com/orandin/slog-gorm"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
gormLogger "gorm.io/gorm/logger"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
sqliteutil "github.com/pocket-id/pocket-id/backend/internal/utils/sqlite"
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
|
||||
func NewDatabase() (db *gorm.DB) {
|
||||
db, err := connectDatabase()
|
||||
func NewDatabase() (db *gorm.DB, err error) {
|
||||
db, err = connectDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect to database: %v", err)
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
sqlDb, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get sql.DB: %v", err)
|
||||
return nil, fmt.Errorf("failed to get sql.DB: %w", err)
|
||||
}
|
||||
|
||||
// Choose the correct driver for the database provider
|
||||
@@ -43,23 +47,24 @@ func NewDatabase() (db *gorm.DB) {
|
||||
driver, err = postgresMigrate.WithInstance(sqlDb, &postgresMigrate.Config{})
|
||||
default:
|
||||
// Should never happen at this point
|
||||
log.Fatalf("unsupported database provider: %s", common.EnvConfig.DbProvider)
|
||||
return nil, fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create migration driver: %v", err)
|
||||
return nil, fmt.Errorf("failed to create migration driver: %w", err)
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
if err := migrateDatabase(driver); err != nil {
|
||||
log.Fatalf("failed to run migrations: %v", err)
|
||||
return nil, fmt.Errorf("failed to run migrations: %w", err)
|
||||
}
|
||||
|
||||
return db
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDatabase(driver database.Driver) error {
|
||||
// Use the embedded migrations
|
||||
source, err := iofs.New(resources.FS, "migrations/"+string(common.EnvConfig.DbProvider))
|
||||
// Embedded migrations via iofs
|
||||
path := "migrations/" + string(common.EnvConfig.DbProvider)
|
||||
source, err := iofs.New(resources.FS, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create embedded migration source: %w", err)
|
||||
}
|
||||
@@ -69,14 +74,66 @@ func migrateDatabase(driver database.Driver) error {
|
||||
return fmt.Errorf("failed to create migration instance: %w", err)
|
||||
}
|
||||
|
||||
err = m.Up()
|
||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("failed to apply migrations: %w", err)
|
||||
requiredVersion, err := getRequiredMigrationVersion(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get last migration version: %w", err)
|
||||
}
|
||||
|
||||
currentVersion, _, _ := m.Version()
|
||||
if currentVersion > requiredVersion {
|
||||
slog.Warn("Database version is newer than the application supports, possible downgrade detected", slog.Uint64("db_version", uint64(currentVersion)), slog.Uint64("app_version", uint64(requiredVersion)))
|
||||
if !common.EnvConfig.AllowDowngrade {
|
||||
return fmt.Errorf("database version (%d) is newer than application version (%d), downgrades are not allowed (set ALLOW_DOWNGRADE=true to enable)", currentVersion, requiredVersion)
|
||||
}
|
||||
slog.Info("Fetching migrations from GitHub to handle possible downgrades")
|
||||
return migrateDatabaseFromGitHub(driver, requiredVersion)
|
||||
}
|
||||
|
||||
if err := m.Migrate(requiredVersion); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("failed to apply embedded migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateDatabaseFromGitHub(driver database.Driver, version uint) error {
|
||||
srcURL := "github://pocket-id/pocket-id/backend/resources/migrations/" + string(common.EnvConfig.DbProvider)
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(srcURL, "pocket-id", driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub migration instance: %w", err)
|
||||
}
|
||||
|
||||
if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("failed to apply GitHub migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRequiredMigrationVersion reads the embedded migration files and returns the highest version number found.
|
||||
func getRequiredMigrationVersion(path string) (uint, error) {
|
||||
entries, err := resources.FS.ReadDir(path)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read migration directory: %w", err)
|
||||
}
|
||||
|
||||
var maxVersion uint
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
var version uint
|
||||
n, err := fmt.Sscanf(name, "%d_", &version)
|
||||
if err == nil && n == 1 {
|
||||
if version > maxVersion {
|
||||
maxVersion = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxVersion, nil
|
||||
}
|
||||
|
||||
func connectDatabase() (db *gorm.DB, err error) {
|
||||
var dialector gorm.Dialector
|
||||
|
||||
@@ -86,14 +143,20 @@ func connectDatabase() (db *gorm.DB, err error) {
|
||||
if common.EnvConfig.DbConnectionString == "" {
|
||||
return nil, errors.New("missing required env var 'DB_CONNECTION_STRING' for SQLite database")
|
||||
}
|
||||
if !strings.HasPrefix(common.EnvConfig.DbConnectionString, "file:") {
|
||||
return nil, errors.New("invalid value for env var 'DB_CONNECTION_STRING': does not begin with 'file:'")
|
||||
}
|
||||
|
||||
sqliteutil.RegisterSqliteFunctions()
|
||||
connString, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
|
||||
|
||||
connString, dbPath, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Before we connect, also make sure that there's a temporary folder for SQLite to write its data
|
||||
err = ensureSqliteTempDir(filepath.Dir(dbPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialector = sqlite.Open(connString)
|
||||
case common.DbProviderPostgres:
|
||||
if common.EnvConfig.DbConnectionString == "" {
|
||||
@@ -107,38 +170,68 @@ func connectDatabase() (db *gorm.DB, err error) {
|
||||
for i := 1; i <= 3; i++ {
|
||||
db, err = gorm.Open(dialector, &gorm.Config{
|
||||
TranslateError: true,
|
||||
Logger: getLogger(),
|
||||
Logger: getGormLogger(),
|
||||
})
|
||||
if err == nil {
|
||||
slog.Info("Connected to database", slog.String("provider", string(common.EnvConfig.DbProvider)))
|
||||
return db, nil
|
||||
}
|
||||
|
||||
log.Printf("Attempt %d: Failed to initialize database. Retrying...", i)
|
||||
slog.Warn("Failed to connect to database, will retry in 3s", slog.Int("attempt", i), slog.String("provider", string(common.EnvConfig.DbProvider)), slog.Any("error", err))
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
slog.Error("Failed to connect to database after 3 attempts", slog.String("provider", string(common.EnvConfig.DbProvider)), slog.Any("error", err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func parseSqliteConnectionString(connString string) (parsedConnString string, dbPath string, err error) {
|
||||
if !strings.HasPrefix(connString, "file:") {
|
||||
connString = "file:" + connString
|
||||
}
|
||||
|
||||
// Check if we're using an in-memory database
|
||||
isMemoryDB := isSqliteInMemory(connString)
|
||||
|
||||
// Parse the connection string
|
||||
connStringUrl, err := url.Parse(connString)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
|
||||
}
|
||||
|
||||
// Convert options for the old SQLite driver to the new one
|
||||
convertSqlitePragmaArgs(connStringUrl)
|
||||
|
||||
// Add the default and required params
|
||||
err = addSqliteDefaultParameters(connStringUrl, isMemoryDB)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid SQLite connection string: %w", err)
|
||||
}
|
||||
|
||||
// Get the absolute path to the database
|
||||
// Here, we know for a fact that the ? is present
|
||||
parsedConnString = connStringUrl.String()
|
||||
idx := strings.IndexRune(parsedConnString, '?')
|
||||
dbPath, err = filepath.Abs(parsedConnString[len("file:"):idx])
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to determine absolute path to the database: %w", err)
|
||||
}
|
||||
|
||||
return parsedConnString, dbPath, nil
|
||||
}
|
||||
|
||||
// The official C implementation of SQLite allows some additional properties in the connection string
|
||||
// that are not supported in the in the modernc.org/sqlite driver, and which must be passed as PRAGMA args instead.
|
||||
// To ensure that people can use similar args as in the C driver, which was also used by Pocket ID
|
||||
// previously (via github.com/mattn/go-sqlite3), we are converting some options.
|
||||
func parseSqliteConnectionString(connString string) (string, error) {
|
||||
if !strings.HasPrefix(connString, "file:") {
|
||||
connString = "file:" + connString
|
||||
}
|
||||
|
||||
connStringUrl, err := url.Parse(connString)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
|
||||
}
|
||||
|
||||
// Note this function updates connStringUrl.
|
||||
func convertSqlitePragmaArgs(connStringUrl *url.URL) {
|
||||
// Reference: https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string
|
||||
// This only includes a subset of options, excluding those that are not relevant to us
|
||||
qs := make(url.Values, len(connStringUrl.Query()))
|
||||
for k, v := range connStringUrl.Query() {
|
||||
switch k {
|
||||
switch strings.ToLower(k) {
|
||||
case "_auto_vacuum", "_vacuum":
|
||||
qs.Add("_pragma", "auto_vacuum("+v[0]+")")
|
||||
case "_busy_timeout", "_timeout":
|
||||
@@ -159,29 +252,186 @@ func parseSqliteConnectionString(connString string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the connStringUrl object
|
||||
connStringUrl.RawQuery = qs.Encode()
|
||||
|
||||
return connStringUrl.String(), nil
|
||||
}
|
||||
|
||||
func getLogger() logger.Interface {
|
||||
isProduction := common.EnvConfig.AppEnv == "production"
|
||||
// Adds the default (and some required) parameters to the SQLite connection string.
|
||||
// Note this function updates connStringUrl.
|
||||
func addSqliteDefaultParameters(connStringUrl *url.URL, isMemoryDB bool) error {
|
||||
// This function include code adapted from https://github.com/dapr/components-contrib/blob/v1.14.6/
|
||||
// Copyright (C) 2023 The Dapr Authors
|
||||
// License: Apache2
|
||||
const defaultBusyTimeout = 2500 * time.Millisecond
|
||||
|
||||
var logLevel logger.LogLevel
|
||||
if isProduction {
|
||||
logLevel = logger.Error
|
||||
} else {
|
||||
logLevel = logger.Info
|
||||
// Get the "query string" from the connection string if present
|
||||
qs := connStringUrl.Query()
|
||||
if len(qs) == 0 {
|
||||
qs = make(url.Values, 2)
|
||||
}
|
||||
|
||||
return logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logLevel,
|
||||
IgnoreRecordNotFoundError: isProduction,
|
||||
ParameterizedQueries: isProduction,
|
||||
Colorful: !isProduction,
|
||||
},
|
||||
)
|
||||
// If the database is in-memory, we must ensure that cache=shared is set
|
||||
if isMemoryDB {
|
||||
qs["cache"] = []string{"shared"}
|
||||
}
|
||||
|
||||
// Check if the database is read-only or immutable
|
||||
isReadOnly := false
|
||||
if len(qs["mode"]) > 0 {
|
||||
// Keep the first value only
|
||||
qs["mode"] = []string{
|
||||
strings.ToLower(qs["mode"][0]),
|
||||
}
|
||||
if qs["mode"][0] == "ro" {
|
||||
isReadOnly = true
|
||||
}
|
||||
}
|
||||
if len(qs["immutable"]) > 0 {
|
||||
// Keep the first value only
|
||||
qs["immutable"] = []string{
|
||||
strings.ToLower(qs["immutable"][0]),
|
||||
}
|
||||
if qs["immutable"][0] == "1" {
|
||||
isReadOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
// We do not want to override a _txlock if set, but we'll show a warning if it's not "immediate"
|
||||
if len(qs["_txlock"]) > 0 {
|
||||
// Keep the first value only
|
||||
qs["_txlock"] = []string{
|
||||
strings.ToLower(qs["_txlock"][0]),
|
||||
}
|
||||
if qs["_txlock"][0] != "immediate" {
|
||||
slog.Warn("SQLite connection is being created with a _txlock different from the recommended value 'immediate'")
|
||||
}
|
||||
} else {
|
||||
qs["_txlock"] = []string{"immediate"}
|
||||
}
|
||||
|
||||
// Add pragma values
|
||||
var hasBusyTimeout, hasJournalMode bool
|
||||
if len(qs["_pragma"]) == 0 {
|
||||
qs["_pragma"] = make([]string, 0, 3)
|
||||
} else {
|
||||
for _, p := range qs["_pragma"] {
|
||||
p = strings.ToLower(p)
|
||||
switch {
|
||||
case strings.HasPrefix(p, "busy_timeout"):
|
||||
hasBusyTimeout = true
|
||||
case strings.HasPrefix(p, "journal_mode"):
|
||||
hasJournalMode = true
|
||||
case strings.HasPrefix(p, "foreign_keys"):
|
||||
return errors.New("found forbidden option '_pragma=foreign_keys' in the connection string")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasBusyTimeout {
|
||||
qs["_pragma"] = append(qs["_pragma"], fmt.Sprintf("busy_timeout(%d)", defaultBusyTimeout.Milliseconds()))
|
||||
}
|
||||
if !hasJournalMode {
|
||||
switch {
|
||||
case isMemoryDB:
|
||||
// For in-memory databases, set the journal to MEMORY, the only allowed option besides OFF (which would make transactions ineffective)
|
||||
qs["_pragma"] = append(qs["_pragma"], "journal_mode(MEMORY)")
|
||||
case isReadOnly:
|
||||
// Set the journaling mode to "DELETE" (the default) if the database is read-only
|
||||
qs["_pragma"] = append(qs["_pragma"], "journal_mode(DELETE)")
|
||||
default:
|
||||
// Enable WAL
|
||||
qs["_pragma"] = append(qs["_pragma"], "journal_mode(WAL)")
|
||||
}
|
||||
}
|
||||
|
||||
// Forcefully enable foreign keys
|
||||
qs["_pragma"] = append(qs["_pragma"], "foreign_keys(1)")
|
||||
|
||||
// Update the connStringUrl object
|
||||
connStringUrl.RawQuery = qs.Encode()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSqliteInMemory returns true if the connection string is for an in-memory database.
|
||||
func isSqliteInMemory(connString string) bool {
|
||||
lc := strings.ToLower(connString)
|
||||
|
||||
// First way to define an in-memory database is to use ":memory:" or "file::memory:" as connection string
|
||||
if strings.HasPrefix(lc, ":memory:") || strings.HasPrefix(lc, "file::memory:") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Another way is to pass "mode=memory" in the "query string"
|
||||
idx := strings.IndexRune(lc, '?')
|
||||
if idx < 0 {
|
||||
return false
|
||||
}
|
||||
qs, _ := url.ParseQuery(lc[(idx + 1):])
|
||||
|
||||
return len(qs["mode"]) > 0 && qs["mode"][0] == "memory"
|
||||
}
|
||||
|
||||
// ensureSqliteTempDir ensures that SQLite has a directory where it can write temporary files if needed
|
||||
// The default directory may not be writable when using a container with a read-only root file system
|
||||
// See: https://www.sqlite.org/tempfiles.html
|
||||
func ensureSqliteTempDir(dbPath string) error {
|
||||
// Per docs, SQLite tries these folders in order (excluding those that aren't applicable to us):
|
||||
//
|
||||
// - The SQLITE_TMPDIR environment variable
|
||||
// - The TMPDIR environment variable
|
||||
// - /var/tmp
|
||||
// - /usr/tmp
|
||||
// - /tmp
|
||||
//
|
||||
// Source: https://www.sqlite.org/tempfiles.html#temporary_file_storage_locations
|
||||
//
|
||||
// First, let's check if SQLITE_TMPDIR or TMPDIR are set, in which case we trust the user has taken care of the problem already
|
||||
if os.Getenv("SQLITE_TMPDIR") != "" || os.Getenv("TMPDIR") != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now, let's check if /var/tmp, /usr/tmp, or /tmp exist and are writable
|
||||
for _, dir := range []string{"/var/tmp", "/usr/tmp", "/tmp"} {
|
||||
ok, err := utils.IsWritableDir(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if %s is writable: %w", dir, err)
|
||||
}
|
||||
if ok {
|
||||
// We found a folder that's writable
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we're here, there's no temporary directory that's writable (not unusual for containers with a read-only root file system), so we set SQLITE_TMPDIR to the folder where the SQLite database is set
|
||||
err := os.Setenv("SQLITE_TMPDIR", dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set SQLITE_TMPDIR environmental variable: %w", err)
|
||||
}
|
||||
|
||||
slog.Debug("Set SQLITE_TMPDIR to the database directory", "path", dbPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getGormLogger() gormLogger.Interface {
|
||||
loggerOpts := make([]slogGorm.Option, 0, 5)
|
||||
loggerOpts = append(loggerOpts,
|
||||
slogGorm.WithSlowThreshold(200*time.Millisecond),
|
||||
slogGorm.WithErrorField("error"),
|
||||
)
|
||||
|
||||
if common.EnvConfig.AppEnv == "production" {
|
||||
loggerOpts = append(loggerOpts,
|
||||
slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelWarn),
|
||||
slogGorm.WithIgnoreTrace(),
|
||||
)
|
||||
} else {
|
||||
loggerOpts = append(loggerOpts,
|
||||
slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelDebug),
|
||||
slogGorm.WithRecordNotFoundError(),
|
||||
slogGorm.WithTraceAll(),
|
||||
)
|
||||
}
|
||||
|
||||
return slogGorm.New(loggerOpts...)
|
||||
}
|
||||
|
||||
@@ -8,23 +8,93 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseSqliteConnectionString(t *testing.T) {
|
||||
func TestIsSqliteInMemory(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedError bool
|
||||
name string
|
||||
connStr string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "memory database with :memory:",
|
||||
connStr: ":memory:",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "memory database with file::memory:",
|
||||
connStr: "file::memory:",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "memory database with :MEMORY: (uppercase)",
|
||||
connStr: ":MEMORY:",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "memory database with FILE::MEMORY: (uppercase)",
|
||||
connStr: "FILE::MEMORY:",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "memory database with mixed case",
|
||||
connStr: ":Memory:",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has mode=memory",
|
||||
connStr: "file:data?mode=memory",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "file database",
|
||||
connStr: "data.db",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "file database with path",
|
||||
connStr: "/path/to/data.db",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "file database with file: prefix",
|
||||
connStr: "file:data.db",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
connStr: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "string containing memory but not at start",
|
||||
connStr: "data:memory:.db",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has mode=ro",
|
||||
connStr: "file:data?mode=ro",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isSqliteInMemory(tt.connStr)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertSqlitePragmaArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic file path",
|
||||
input: "file:test.db",
|
||||
expected: "file:test.db",
|
||||
},
|
||||
{
|
||||
name: "adds file: prefix if missing",
|
||||
input: "test.db",
|
||||
expected: "file:test.db",
|
||||
},
|
||||
{
|
||||
name: "converts _busy_timeout to pragma",
|
||||
input: "file:test.db?_busy_timeout=5000",
|
||||
@@ -100,46 +170,161 @@ func TestParseSqliteConnectionString(t *testing.T) {
|
||||
input: "file:test.db?_fk=1&mode=rw&_timeout=5000",
|
||||
expected: "file:test.db?_pragma=foreign_keys%281%29&_pragma=busy_timeout%285000%29&mode=rw",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resultURL, _ := url.Parse(tt.input)
|
||||
convertSqlitePragmaArgs(resultURL)
|
||||
|
||||
// Parse both URLs to compare components independently
|
||||
expectedURL, err := url.Parse(tt.expected)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Compare scheme and path components
|
||||
compareQueryStrings(t, expectedURL, resultURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSqliteDefaultParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
isMemoryDB bool
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "invalid URL format",
|
||||
input: "file:invalid#$%^&*@test.db",
|
||||
expectedError: true,
|
||||
name: "basic file database",
|
||||
input: "file:test.db",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate",
|
||||
},
|
||||
{
|
||||
name: "in-memory database",
|
||||
input: "file::memory:",
|
||||
isMemoryDB: true,
|
||||
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate&cache=shared",
|
||||
},
|
||||
{
|
||||
name: "read-only database with mode=ro",
|
||||
input: "file:test.db?mode=ro",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&mode=ro",
|
||||
},
|
||||
{
|
||||
name: "immutable database",
|
||||
input: "file:test.db?immutable=1",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&immutable=1",
|
||||
},
|
||||
{
|
||||
name: "database with existing _txlock",
|
||||
input: "file:test.db?_txlock=deferred",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=deferred",
|
||||
},
|
||||
{
|
||||
name: "database with existing busy_timeout pragma",
|
||||
input: "file:test.db?_pragma=busy_timeout%285000%29",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%285000%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate",
|
||||
},
|
||||
{
|
||||
name: "database with existing journal_mode pragma",
|
||||
input: "file:test.db?_pragma=journal_mode%28DELETE%29",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate",
|
||||
},
|
||||
{
|
||||
name: "database with forbidden foreign_keys pragma",
|
||||
input: "file:test.db?_pragma=foreign_keys%280%29",
|
||||
isMemoryDB: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "database with multiple existing pragmas",
|
||||
input: "file:test.db?_pragma=busy_timeout%283000%29&_pragma=journal_mode%28TRUNCATE%29&_pragma=synchronous%28NORMAL%29",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%283000%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28TRUNCATE%29&_pragma=synchronous%28NORMAL%29&_txlock=immediate",
|
||||
},
|
||||
{
|
||||
name: "in-memory database with cache already set",
|
||||
input: "file::memory:?cache=private",
|
||||
isMemoryDB: true,
|
||||
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate&cache=shared",
|
||||
},
|
||||
{
|
||||
name: "database with mode=rw (not read-only)",
|
||||
input: "file:test.db?mode=rw",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate&mode=rw",
|
||||
},
|
||||
{
|
||||
name: "database with immutable=0 (not immutable)",
|
||||
input: "file:test.db?immutable=0",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_txlock=immediate&immutable=0",
|
||||
},
|
||||
{
|
||||
name: "database with mixed case mode=RO",
|
||||
input: "file:test.db?mode=RO",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&mode=ro",
|
||||
},
|
||||
{
|
||||
name: "database with mixed case immutable=1",
|
||||
input: "file:test.db?immutable=1",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28DELETE%29&_txlock=immediate&immutable=1",
|
||||
},
|
||||
{
|
||||
name: "complex database configuration",
|
||||
input: "file:test.db?cache=shared&mode=rwc&_txlock=immediate&_pragma=synchronous%28FULL%29",
|
||||
isMemoryDB: false,
|
||||
expected: "file:test.db?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28WAL%29&_pragma=synchronous%28FULL%29&_txlock=immediate&cache=shared&mode=rwc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseSqliteConnectionString(tt.input)
|
||||
resultURL, err := url.Parse(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.expectedError {
|
||||
err = addSqliteDefaultParameters(resultURL, tt.isMemoryDB)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse both URLs to compare components independently
|
||||
expectedURL, err := url.Parse(tt.expected)
|
||||
require.NoError(t, err)
|
||||
|
||||
resultURL, err := url.Parse(result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Compare scheme and path components
|
||||
assert.Equal(t, expectedURL.Scheme, resultURL.Scheme)
|
||||
assert.Equal(t, expectedURL.Path, resultURL.Path)
|
||||
|
||||
// Compare query parameters regardless of order
|
||||
expectedQuery := expectedURL.Query()
|
||||
resultQuery := resultURL.Query()
|
||||
|
||||
assert.Len(t, expectedQuery, len(resultQuery))
|
||||
|
||||
for key, expectedValues := range expectedQuery {
|
||||
resultValues, ok := resultQuery[key]
|
||||
_ = assert.True(t, ok) &&
|
||||
assert.ElementsMatch(t, expectedValues, resultValues)
|
||||
}
|
||||
compareQueryStrings(t, expectedURL, resultURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func compareQueryStrings(t *testing.T, expectedURL *url.URL, resultURL *url.URL) {
|
||||
t.Helper()
|
||||
|
||||
// Compare scheme and path components
|
||||
assert.Equal(t, expectedURL.Scheme, resultURL.Scheme)
|
||||
assert.Equal(t, expectedURL.Path, resultURL.Path)
|
||||
|
||||
// Compare query parameters regardless of order
|
||||
expectedQuery := expectedURL.Query()
|
||||
resultQuery := resultURL.Query()
|
||||
|
||||
assert.Len(t, expectedQuery, len(resultQuery))
|
||||
|
||||
for key, expectedValues := range expectedQuery {
|
||||
resultValues, ok := resultQuery[key]
|
||||
_ = assert.True(t, ok) &&
|
||||
assert.ElementsMatch(t, expectedValues, resultValues)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -18,7 +19,8 @@ func init() {
|
||||
func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
|
||||
testService, err := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize test service: %v", err)
|
||||
slog.Error("Failed to initialize test service", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
210
backend/internal/bootstrap/observability_boostrap.go
Normal file
210
backend/internal/bootstrap/observability_boostrap.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lmittmann/tint"
|
||||
"github.com/mattn/go-isatty"
|
||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||
"go.opentelemetry.io/contrib/exporters/autoexport"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel"
|
||||
globallog "go.opentelemetry.io/otel/log/global"
|
||||
metricnoop "go.opentelemetry.io/otel/metric/noop"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
|
||||
tracenoop "go.opentelemetry.io/otel/trace/noop"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
func defaultResource() (*resource.Resource, error) {
|
||||
return resource.Merge(
|
||||
resource.Default(),
|
||||
resource.NewSchemaless(
|
||||
semconv.ServiceName(common.Name),
|
||||
semconv.ServiceVersion(common.Version),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func initObservability(ctx context.Context, metrics, traces bool) (shutdownFns []utils.Service, httpClient *http.Client, err error) {
|
||||
resource, err := defaultResource()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create OpenTelemetry resource: %w", err)
|
||||
}
|
||||
|
||||
shutdownFns = make([]utils.Service, 0, 2)
|
||||
|
||||
httpClient = &http.Client{}
|
||||
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
// Indicates a development-time error
|
||||
panic("Default transport is not of type *http.Transport")
|
||||
}
|
||||
httpClient.Transport = defaultTransport.Clone()
|
||||
|
||||
// Logging
|
||||
err = initOtelLogging(ctx, resource)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Tracing
|
||||
tracingShutdownFn, err := initOtelTracing(ctx, traces, resource, httpClient)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if tracingShutdownFn != nil {
|
||||
shutdownFns = append(shutdownFns, tracingShutdownFn)
|
||||
}
|
||||
|
||||
// Metrics
|
||||
metricsShutdownFn, err := initOtelMetrics(ctx, metrics, resource)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if metricsShutdownFn != nil {
|
||||
shutdownFns = append(shutdownFns, metricsShutdownFn)
|
||||
}
|
||||
|
||||
return shutdownFns, httpClient, nil
|
||||
}
|
||||
|
||||
func initOtelLogging(ctx context.Context, resource *resource.Resource) error {
|
||||
// If the env var OTEL_LOGS_EXPORTER is empty, we set it to "none", for autoexport to work
|
||||
if os.Getenv("OTEL_LOGS_EXPORTER") == "" {
|
||||
os.Setenv("OTEL_LOGS_EXPORTER", "none")
|
||||
}
|
||||
exp, err := autoexport.NewLogExporter(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize OpenTelemetry log exporter: %w", err)
|
||||
}
|
||||
|
||||
level := slog.LevelDebug
|
||||
if common.EnvConfig.AppEnv == "production" {
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
|
||||
// Create the handler
|
||||
var handler slog.Handler
|
||||
switch {
|
||||
case common.EnvConfig.LogJSON:
|
||||
// Log as JSON if configured
|
||||
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
case isatty.IsTerminal(os.Stdout.Fd()):
|
||||
// Enable colors if we have a TTY
|
||||
handler = tint.NewHandler(os.Stdout, &tint.Options{
|
||||
TimeFormat: time.StampMilli,
|
||||
Level: level,
|
||||
})
|
||||
default:
|
||||
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
|
||||
// Create the logger provider
|
||||
provider := sdklog.NewLoggerProvider(
|
||||
sdklog.WithProcessor(
|
||||
sdklog.NewBatchProcessor(exp),
|
||||
),
|
||||
sdklog.WithResource(resource),
|
||||
)
|
||||
|
||||
// Set the logger provider globally
|
||||
globallog.SetLoggerProvider(provider)
|
||||
|
||||
// Wrap the handler in a "fanout" one
|
||||
handler = utils.LogFanoutHandler{
|
||||
handler,
|
||||
otelslog.NewHandler(common.Name, otelslog.WithLoggerProvider(provider)),
|
||||
}
|
||||
|
||||
// Set the default slog to send logs to OTel and add the app name
|
||||
log := slog.New(handler).
|
||||
With(slog.String("app", common.Name)).
|
||||
With(slog.String("version", common.Version))
|
||||
slog.SetDefault(log)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initOtelTracing(ctx context.Context, traces bool, resource *resource.Resource, httpClient *http.Client) (shutdownFn utils.Service, err error) {
|
||||
if !traces {
|
||||
otel.SetTracerProvider(tracenoop.NewTracerProvider())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tr, err := autoexport.NewSpanExporter(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize OpenTelemetry span exporter: %w", err)
|
||||
}
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithResource(resource),
|
||||
sdktrace.WithBatcher(tr),
|
||||
)
|
||||
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(
|
||||
propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
),
|
||||
)
|
||||
|
||||
shutdownFn = func(shutdownCtx context.Context) error { //nolint:contextcheck
|
||||
tpCtx, tpCancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||
defer tpCancel()
|
||||
shutdownErr := tp.Shutdown(tpCtx)
|
||||
if shutdownErr != nil {
|
||||
return fmt.Errorf("failed to gracefully shut down traces exporter: %w", shutdownErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add tracing to the HTTP client
|
||||
httpClient.Transport = otelhttp.NewTransport(httpClient.Transport)
|
||||
|
||||
return shutdownFn, nil
|
||||
}
|
||||
|
||||
func initOtelMetrics(ctx context.Context, metrics bool, resource *resource.Resource) (shutdownFn utils.Service, err error) {
|
||||
if !metrics {
|
||||
otel.SetMeterProvider(metricnoop.NewMeterProvider())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mr, err := autoexport.NewMetricReader(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize OpenTelemetry metric reader: %w", err)
|
||||
}
|
||||
|
||||
mp := metric.NewMeterProvider(
|
||||
metric.WithResource(resource),
|
||||
metric.WithReader(mr),
|
||||
)
|
||||
otel.SetMeterProvider(mp)
|
||||
|
||||
shutdownFn = func(shutdownCtx context.Context) error { //nolint:contextcheck
|
||||
mpCtx, mpCancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||
defer mpCancel()
|
||||
shutdownErr := mp.Shutdown(mpCtx)
|
||||
if shutdownErr != nil {
|
||||
return fmt.Errorf("failed to gracefully shut down metrics exporter: %w", shutdownErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return shutdownFn, nil
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"go.opentelemetry.io/contrib/exporters/autoexport"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel"
|
||||
metricnoop "go.opentelemetry.io/otel/metric/noop"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
|
||||
tracenoop "go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
func defaultResource() (*resource.Resource, error) {
|
||||
return resource.Merge(
|
||||
resource.Default(),
|
||||
resource.NewSchemaless(
|
||||
semconv.ServiceName("pocket-id-backend"),
|
||||
semconv.ServiceVersion(common.Version),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func initOtel(ctx context.Context, metrics, traces bool) (shutdownFns []utils.Service, httpClient *http.Client, err error) {
|
||||
resource, err := defaultResource()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create OpenTelemetry resource: %w", err)
|
||||
}
|
||||
|
||||
shutdownFns = make([]utils.Service, 0, 2)
|
||||
|
||||
httpClient = &http.Client{}
|
||||
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
// Indicates a development-time error
|
||||
panic("Default transport is not of type *http.Transport")
|
||||
}
|
||||
httpClient.Transport = defaultTransport.Clone()
|
||||
|
||||
if traces {
|
||||
tr, err := autoexport.NewSpanExporter(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to initialize OpenTelemetry span exporter: %w", err)
|
||||
}
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithResource(resource),
|
||||
sdktrace.WithBatcher(tr),
|
||||
)
|
||||
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(
|
||||
propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
),
|
||||
)
|
||||
|
||||
shutdownFns = append(shutdownFns, func(shutdownCtx context.Context) error { //nolint:contextcheck
|
||||
tpCtx, tpCancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||
defer tpCancel()
|
||||
shutdownErr := tp.Shutdown(tpCtx)
|
||||
if shutdownErr != nil {
|
||||
return fmt.Errorf("failed to gracefully shut down traces exporter: %w", shutdownErr)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
httpClient.Transport = otelhttp.NewTransport(httpClient.Transport)
|
||||
} else {
|
||||
otel.SetTracerProvider(tracenoop.NewTracerProvider())
|
||||
}
|
||||
|
||||
if metrics {
|
||||
mr, err := autoexport.NewMetricReader(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to initialize OpenTelemetry metric reader: %w", err)
|
||||
}
|
||||
mp := metric.NewMeterProvider(
|
||||
metric.WithResource(resource),
|
||||
metric.WithReader(mr),
|
||||
)
|
||||
|
||||
otel.SetMeterProvider(mp)
|
||||
shutdownFns = append(shutdownFns, func(shutdownCtx context.Context) error { //nolint:contextcheck
|
||||
mpCtx, mpCancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||
defer mpCancel()
|
||||
shutdownErr := mp.Shutdown(mpCtx)
|
||||
if shutdownErr != nil {
|
||||
return fmt.Errorf("failed to gracefully shut down metrics exporter: %w", shutdownErr)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
otel.SetMeterProvider(metricnoop.NewMeterProvider())
|
||||
}
|
||||
|
||||
return shutdownFns, httpClient, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/frontend"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
sloggin "github.com/samber/slog-gin"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
"golang.org/x/time/rate"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/frontend"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/controller"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
@@ -32,7 +32,8 @@ var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *
|
||||
func initRouter(db *gorm.DB, svc *services) utils.Service {
|
||||
runner, err := initRouterInternal(db, svc)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init router: %v", err)
|
||||
slog.Error("Failed to init router", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return runner
|
||||
}
|
||||
@@ -60,21 +61,25 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{Skip: func(c *gin.Context) bool {
|
||||
for _, prefix := range loggerSkipPathsPrefix {
|
||||
if strings.HasPrefix(c.Request.Method+" "+c.Request.URL.String(), prefix) {
|
||||
r.Use(sloggin.NewWithConfig(slog.Default(), sloggin.Config{
|
||||
Filters: []sloggin.Filter{
|
||||
func(c *gin.Context) bool {
|
||||
for _, prefix := range loggerSkipPathsPrefix {
|
||||
if strings.HasPrefix(c.Request.Method+" "+c.Request.URL.String(), prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}}))
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
if !common.EnvConfig.TrustProxy {
|
||||
_ = r.SetTrustedProxies(nil)
|
||||
}
|
||||
|
||||
if common.EnvConfig.TracingEnabled {
|
||||
r.Use(otelgin.Middleware("pocket-id-backend"))
|
||||
r.Use(otelgin.Middleware(common.Name))
|
||||
}
|
||||
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60)
|
||||
@@ -85,7 +90,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
|
||||
err := frontend.RegisterFrontend(r)
|
||||
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||
log.Println("Frontend is not included in the build. Skipping frontend registration.")
|
||||
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to register frontend: %w", err)
|
||||
}
|
||||
@@ -135,7 +140,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
addr = common.EnvConfig.UnixSocket
|
||||
}
|
||||
|
||||
listener, err := net.Listen(network, addr)
|
||||
listener, err := net.Listen(network, addr) //nolint:noctx
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
||||
}
|
||||
@@ -154,7 +159,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
|
||||
// Service runner function
|
||||
runFn := func(ctx context.Context) error {
|
||||
log.Printf("Server listening on %s", addr)
|
||||
slog.Info("Server listening", slog.String("addr", addr))
|
||||
|
||||
// Start the server in a background goroutine
|
||||
go func() {
|
||||
@@ -163,7 +168,8 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
// Next call blocks until the server is shut down
|
||||
srvErr := srv.Serve(listener)
|
||||
if srvErr != http.ErrServerClosed {
|
||||
log.Fatalf("Error starting app server: %v", srvErr)
|
||||
slog.Error("Error starting app server", "error", srvErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -171,7 +177,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
err = systemd.SdNotifyReady()
|
||||
if err != nil {
|
||||
// Log the error only
|
||||
log.Printf("[WARN] Unable to notify systemd that the service is ready: %v", err)
|
||||
slog.Warn("Unable to notify systemd that the service is ready", "error", err)
|
||||
}
|
||||
|
||||
// Block until the context is canceled
|
||||
@@ -184,7 +190,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
shutdownCancel()
|
||||
if shutdownErr != nil {
|
||||
// Log the error only (could be context canceled)
|
||||
log.Printf("[WARN] App server shutdown error: %v", shutdownErr)
|
||||
slog.Warn("App server shutdown error", "error", shutdownErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -29,7 +29,10 @@ type services struct {
|
||||
func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client) (svc *services, err error) {
|
||||
svc = &services{}
|
||||
|
||||
svc.appConfigService = service.NewAppConfigService(ctx, db)
|
||||
svc.appConfigService, err = service.NewAppConfigService(ctx, db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create app config service: %w", err)
|
||||
}
|
||||
|
||||
svc.emailService, err = service.NewEmailService(db, svc.appConfigService)
|
||||
if err != nil {
|
||||
@@ -38,19 +41,26 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client) (sv
|
||||
|
||||
svc.geoLiteService = service.NewGeoLiteService(httpClient)
|
||||
svc.auditLogService = service.NewAuditLogService(db, svc.appConfigService, svc.emailService, svc.geoLiteService)
|
||||
svc.jwtService = service.NewJwtService(db, svc.appConfigService)
|
||||
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
|
||||
svc.customClaimService = service.NewCustomClaimService(db)
|
||||
svc.jwtService, err = service.NewJwtService(db, svc.appConfigService)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JWT service: %w", err)
|
||||
}
|
||||
|
||||
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService)
|
||||
svc.customClaimService = service.NewCustomClaimService(db)
|
||||
svc.webauthnService, err = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
|
||||
}
|
||||
|
||||
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
|
||||
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService)
|
||||
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
|
||||
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
|
||||
svc.webauthnService = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ func init() {
|
||||
Use: "key-rotate",
|
||||
Short: "Generates a new token signing key and replaces the current one",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
db := bootstrap.NewDatabase()
|
||||
db, err := bootstrap.NewDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return keyRotate(cmd.Context(), flags, db, &common.EnvConfig)
|
||||
},
|
||||
@@ -80,7 +83,10 @@ func keyRotate(ctx context.Context, flags keyRotateFlags, db *gorm.DB, envConfig
|
||||
}
|
||||
|
||||
// Init the services we need
|
||||
appConfigService := service.NewAppConfigService(ctx, db)
|
||||
appConfigService, err := service.NewAppConfigService(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create app config service: %w", err)
|
||||
}
|
||||
|
||||
// Get the key provider
|
||||
keyProvider, err := jwkutils.GetKeyProvider(db, envConfig, appConfigService.GetDbConfig().InstanceID.Value)
|
||||
|
||||
@@ -97,7 +97,8 @@ func testKeyRotateWithFileStorage(t *testing.T, flags keyRotateFlags, wantErr bo
|
||||
db := testingutils.NewDatabaseForTest(t)
|
||||
|
||||
// Initialize app config service and create instance
|
||||
appConfigService := service.NewAppConfigService(t.Context(), db)
|
||||
appConfigService, err := service.NewAppConfigService(t.Context(), db)
|
||||
require.NoError(t, err)
|
||||
instanceID := appConfigService.GetDbConfig().InstanceID.Value
|
||||
|
||||
// Check if key exists before rotation
|
||||
@@ -140,14 +141,15 @@ func testKeyRotateWithDatabaseStorage(t *testing.T, flags keyRotateFlags, wantEr
|
||||
// Set up database storage config
|
||||
envConfig := &common.EnvConfigSchema{
|
||||
KeysStorage: "database",
|
||||
EncryptionKey: "test-encryption-key-characters-long",
|
||||
EncryptionKey: []byte("test-encryption-key-characters-long"),
|
||||
}
|
||||
|
||||
// Create test database
|
||||
db := testingutils.NewDatabaseForTest(t)
|
||||
|
||||
// Initialize app config service and create instance
|
||||
appConfigService := service.NewAppConfigService(t.Context(), db)
|
||||
appConfigService, err := service.NewAppConfigService(t.Context(), db)
|
||||
require.NoError(t, err)
|
||||
instanceID := appConfigService.GetDbConfig().InstanceID.Value
|
||||
|
||||
// Get key provider
|
||||
|
||||
@@ -24,11 +24,14 @@ var oneTimeAccessTokenCmd = &cobra.Command{
|
||||
userArg := args[0]
|
||||
|
||||
// Connect to the database
|
||||
db := bootstrap.NewDatabase()
|
||||
db, err := bootstrap.NewDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the access token
|
||||
var oneTimeAccessToken *model.OneTimeAccessToken
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
// Load the user to retrieve the user ID
|
||||
var user model.User
|
||||
queryCtx, queryCancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
||||
@@ -48,7 +51,7 @@ var oneTimeAccessTokenCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// Create a new access token that expires in 1 hour
|
||||
oneTimeAccessToken, txErr = service.NewOneTimeAccessToken(user.ID, time.Now().Add(time.Hour))
|
||||
oneTimeAccessToken, txErr = service.NewOneTimeAccessToken(user.ID, time.Hour)
|
||||
if txErr != nil {
|
||||
return fmt.Errorf("failed to generate access token: %w", txErr)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ package common
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
@@ -23,32 +26,33 @@ const (
|
||||
DbProviderSqlite DbProvider = "sqlite"
|
||||
DbProviderPostgres DbProvider = "postgres"
|
||||
MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz"
|
||||
defaultSqliteConnString string = "file:data/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate"
|
||||
defaultSqliteConnString string = "data/pocket-id.db"
|
||||
)
|
||||
|
||||
type EnvConfigSchema struct {
|
||||
AppEnv string `env:"APP_ENV"`
|
||||
AppURL string `env:"APP_URL"`
|
||||
DbProvider DbProvider `env:"DB_PROVIDER"`
|
||||
DbConnectionString string `env:"DB_CONNECTION_STRING"`
|
||||
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
|
||||
UploadPath string `env:"UPLOAD_PATH"`
|
||||
KeysPath string `env:"KEYS_PATH"`
|
||||
KeysStorage string `env:"KEYS_STORAGE"`
|
||||
EncryptionKey string `env:"ENCRYPTION_KEY"`
|
||||
EncryptionKeyFile string `env:"ENCRYPTION_KEY_FILE"`
|
||||
EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"`
|
||||
Port string `env:"PORT"`
|
||||
Host string `env:"HOST"`
|
||||
UnixSocket string `env:"UNIX_SOCKET"`
|
||||
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
|
||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
|
||||
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
|
||||
UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"`
|
||||
MetricsEnabled bool `env:"METRICS_ENABLED"`
|
||||
TracingEnabled bool `env:"TRACING_ENABLED"`
|
||||
LogJSON bool `env:"LOG_JSON"`
|
||||
TrustProxy bool `env:"TRUST_PROXY"`
|
||||
AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"`
|
||||
AllowDowngrade bool `env:"ALLOW_DOWNGRADE"`
|
||||
}
|
||||
|
||||
var EnvConfig = defaultConfig()
|
||||
@@ -56,7 +60,8 @@ var EnvConfig = defaultConfig()
|
||||
func init() {
|
||||
err := parseEnvConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Configuration error: %v", err)
|
||||
slog.Error("Configuration error", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +73,7 @@ func defaultConfig() EnvConfigSchema {
|
||||
UploadPath: "data/uploads",
|
||||
KeysPath: "data/keys",
|
||||
KeysStorage: "", // "database" or "file"
|
||||
EncryptionKey: "",
|
||||
EncryptionKey: nil,
|
||||
AppURL: "http://localhost:1411",
|
||||
Port: "1411",
|
||||
Host: "0.0.0.0",
|
||||
@@ -83,15 +88,29 @@ func defaultConfig() EnvConfigSchema {
|
||||
TracingEnabled: false,
|
||||
TrustProxy: false,
|
||||
AnalyticsDisabled: false,
|
||||
AllowDowngrade: false,
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnvConfig() error {
|
||||
err := env.ParseWithOptions(&EnvConfig, env.Options{})
|
||||
parsers := map[reflect.Type]env.ParserFunc{
|
||||
reflect.TypeOf([]byte{}): func(value string) (interface{}, error) {
|
||||
return []byte(value), nil
|
||||
},
|
||||
}
|
||||
|
||||
err := env.ParseWithOptions(&EnvConfig, env.Options{
|
||||
FuncMap: parsers,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing env config: %w", err)
|
||||
}
|
||||
|
||||
err = resolveFileBasedEnvVariables(&EnvConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate the environment variables
|
||||
switch EnvConfig.DbProvider {
|
||||
case DbProviderSqlite:
|
||||
@@ -119,9 +138,8 @@ func parseEnvConfig() error {
|
||||
case "":
|
||||
EnvConfig.KeysStorage = "file"
|
||||
case "database":
|
||||
// If KeysStorage is "database", a key must be specified
|
||||
if EnvConfig.EncryptionKey == "" && EnvConfig.EncryptionKeyFile == "" {
|
||||
return errors.New("ENCRYPTION_KEY or ENCRYPTION_KEY_FILE must be non-empty when KEYS_STORAGE is database")
|
||||
if EnvConfig.EncryptionKey == nil {
|
||||
return errors.New("ENCRYPTION_KEY must be non-empty when KEYS_STORAGE is database")
|
||||
}
|
||||
case "file":
|
||||
// All good, these are valid values
|
||||
@@ -131,3 +149,58 @@ func parseEnvConfig() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveFileBasedEnvVariables uses reflection to automatically resolve file-based secrets
|
||||
func resolveFileBasedEnvVariables(config *EnvConfigSchema) error {
|
||||
val := reflect.ValueOf(config).Elem()
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// Only process string and []byte fields
|
||||
isString := field.Kind() == reflect.String
|
||||
isByteSlice := field.Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Uint8
|
||||
if !isString && !isByteSlice {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only process fields with the "options" tag set to "file"
|
||||
optionsTag := fieldType.Tag.Get("options")
|
||||
if optionsTag != "file" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only process fields with the "env" tag
|
||||
envTag := fieldType.Tag.Get("env")
|
||||
if envTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
envVarName := envTag
|
||||
if commaIndex := len(envTag); commaIndex > 0 {
|
||||
envVarName = envTag[:commaIndex]
|
||||
}
|
||||
|
||||
// If the file environment variable is not set, skip
|
||||
envVarFileName := envVarName + "_FILE"
|
||||
envVarFileValue := os.Getenv(envVarFileName)
|
||||
if envVarFileValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileContent, err := os.ReadFile(envVarFileValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file for env var %s: %w", envVarFileName, err)
|
||||
}
|
||||
|
||||
if isString {
|
||||
field.SetString(strings.TrimSpace(string(fileContent)))
|
||||
} else {
|
||||
field.SetBytes(fileContent)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -110,7 +111,7 @@ func TestParseEnvConfig(t *testing.T) {
|
||||
|
||||
err := parseEnvConfig()
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "ENCRYPTION_KEY or ENCRYPTION_KEY_FILE must be non-empty")
|
||||
assert.ErrorContains(t, err, "ENCRYPTION_KEY must be non-empty when KEYS_STORAGE is database")
|
||||
})
|
||||
|
||||
t.Run("should accept valid KEYS_STORAGE values", func(t *testing.T) {
|
||||
@@ -186,3 +187,119 @@ func TestParseEnvConfig(t *testing.T) {
|
||||
assert.Equal(t, "127.0.0.1", EnvConfig.Host)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveFileBasedEnvVariables(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create test files
|
||||
encryptionKeyFile := tempDir + "/encryption_key.txt"
|
||||
encryptionKeyContent := "test-encryption-key-123"
|
||||
err := os.WriteFile(encryptionKeyFile, []byte(encryptionKeyContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbConnFile := tempDir + "/db_connection.txt"
|
||||
dbConnContent := "postgres://user:pass@localhost/testdb"
|
||||
err = os.WriteFile(dbConnFile, []byte(dbConnContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a binary file for testing binary data handling
|
||||
binaryKeyFile := tempDir + "/binary_key.bin"
|
||||
binaryKeyContent := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}
|
||||
err = os.WriteFile(binaryKeyFile, binaryKeyContent, 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should read file content for fields with options:file tag", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
|
||||
// Set environment variables pointing to files
|
||||
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
||||
t.Setenv("DB_CONNECTION_STRING_FILE", dbConnFile)
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify file contents were read correctly
|
||||
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
||||
assert.Equal(t, dbConnContent, config.DbConnectionString)
|
||||
})
|
||||
|
||||
t.Run("should skip fields without options:file tag", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
originalAppURL := config.AppURL
|
||||
|
||||
// Set a file for a field that doesn't have options:file tag
|
||||
t.Setenv("APP_URL_FILE", "/tmp/nonexistent.txt")
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// AppURL should remain unchanged
|
||||
assert.Equal(t, originalAppURL, config.AppURL)
|
||||
})
|
||||
|
||||
t.Run("should skip non-string fields", func(t *testing.T) {
|
||||
// This test verifies that non-string fields are skipped
|
||||
// We test this indirectly by ensuring the function doesn't error
|
||||
// when processing the actual EnvConfigSchema which has bool fields
|
||||
config := defaultConfig()
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should skip when _FILE environment variable is not set", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
originalEncryptionKey := config.EncryptionKey
|
||||
|
||||
// Don't set ENCRYPTION_KEY_FILE environment variable
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// EncryptionKey should remain unchanged
|
||||
assert.Equal(t, originalEncryptionKey, config.EncryptionKey)
|
||||
})
|
||||
|
||||
t.Run("should handle multiple file-based variables simultaneously", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
|
||||
// Set multiple file environment variables
|
||||
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
||||
t.Setenv("DB_CONNECTION_STRING_FILE", dbConnFile)
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// All should be resolved correctly
|
||||
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
||||
assert.Equal(t, dbConnContent, config.DbConnectionString)
|
||||
})
|
||||
|
||||
t.Run("should handle mixed file and non-file environment variables", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
|
||||
// Set both file and non-file environment variables
|
||||
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// File-based should be resolved, others should remain as set by env parser
|
||||
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
||||
assert.Equal(t, "http://localhost:1411", config.AppURL)
|
||||
})
|
||||
|
||||
t.Run("should handle binary data correctly", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
|
||||
// Set environment variable pointing to binary file
|
||||
t.Setenv("ENCRYPTION_KEY_FILE", binaryKeyFile)
|
||||
|
||||
err := resolveFileBasedEnvVariables(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify binary data was read correctly without corruption
|
||||
assert.Equal(t, binaryKeyContent, config.EncryptionKey)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -350,6 +350,15 @@ func (e *OidcAuthorizationPendingError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type ReauthenticationRequiredError struct{}
|
||||
|
||||
func (e *ReauthenticationRequiredError) Error() string {
|
||||
return "reauthentication required"
|
||||
}
|
||||
func (e *ReauthenticationRequiredError) HttpStatusCode() int {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
|
||||
type OpenSignupDisabledError struct{}
|
||||
|
||||
func (e *OpenSignupDisabledError) Error() string {
|
||||
@@ -359,3 +368,13 @@ func (e *OpenSignupDisabledError) Error() string {
|
||||
func (e *OpenSignupDisabledError) HttpStatusCode() int {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
type ClientIdAlreadyExistsError struct{}
|
||||
|
||||
func (e *ClientIdAlreadyExistsError) Error() string {
|
||||
return "Client ID already in use"
|
||||
}
|
||||
|
||||
func (e *ClientIdAlreadyExistsError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package common
|
||||
|
||||
// Name is the name of the application
|
||||
const Name = "pocket-id"
|
||||
|
||||
// Version contains the Pocket ID version.
|
||||
//
|
||||
// It can be set at build time using -ldflags.
|
||||
|
||||
@@ -40,7 +40,7 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := tc.TestService.ResetApplicationImages(); err != nil {
|
||||
if err := tc.TestService.ResetApplicationImages(c.Request.Context()); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -55,8 +55,13 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
group.POST("/oidc/device/verify", authMiddleware.WithAdminNotRequired().Add(), oc.verifyDeviceCodeHandler)
|
||||
group.GET("/oidc/device/info", authMiddleware.WithAdminNotRequired().Add(), oc.getDeviceCodeInfoHandler)
|
||||
|
||||
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
|
||||
group.GET("/oidc/users/:id/clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
|
||||
group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
|
||||
group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
|
||||
|
||||
group.DELETE("/oidc/users/me/authorized-clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler)
|
||||
|
||||
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAccessibleClientsHandler)
|
||||
|
||||
}
|
||||
|
||||
type OidcController struct {
|
||||
@@ -257,7 +262,7 @@ func (oc *OidcController) EndSessionHandler(c *gin.Context) {
|
||||
callbackURL, err := oc.oidcService.ValidateEndSession(c.Request.Context(), input, c.GetString("userID"))
|
||||
if err != nil {
|
||||
// If the validation fails, the user has to confirm the logout manually and doesn't get redirected
|
||||
log.Printf("Error getting logout callback URL, the user has to confirm the logout manually: %v", err)
|
||||
slog.WarnContext(c.Request.Context(), "Error getting logout callback URL, the user has to confirm the logout manually", "error", err)
|
||||
c.Redirect(http.StatusFound, common.EnvConfig.AppURL+"/logout")
|
||||
return
|
||||
}
|
||||
@@ -487,11 +492,11 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Client ID"
|
||||
// @Param client body dto.OidcClientCreateDto true "Client information"
|
||||
// @Param client body dto.OidcClientUpdateDto true "Client information"
|
||||
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client"
|
||||
// @Router /api/oidc/clients/{id} [put]
|
||||
func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
||||
var input dto.OidcClientCreateDto
|
||||
var input dto.OidcClientUpdateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -657,7 +662,7 @@ func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
|
||||
// @Param sort[column] query string false "Column to sort by"
|
||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
|
||||
// @Router /api/oidc/users/me/clients [get]
|
||||
// @Router /api/oidc/users/me/authorized-clients [get]
|
||||
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
oc.listAuthorizedClients(c, userID)
|
||||
@@ -673,7 +678,7 @@ func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
|
||||
// @Param sort[column] query string false "Column to sort by"
|
||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
|
||||
// @Router /api/oidc/users/{id}/clients [get]
|
||||
// @Router /api/oidc/users/{id}/authorized-clients [get]
|
||||
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
oc.listAuthorizedClients(c, userID)
|
||||
@@ -704,6 +709,58 @@ func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
|
||||
})
|
||||
}
|
||||
|
||||
// revokeOwnClientAuthorizationHandler godoc
|
||||
// @Summary Revoke authorization for an OIDC client
|
||||
// @Description Revoke the authorization for a specific OIDC client for the current user
|
||||
// @Tags OIDC
|
||||
// @Param clientId path string true "Client ID to revoke authorization for"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/oidc/users/me/authorized-clients/{clientId} [delete]
|
||||
func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
|
||||
clientID := c.Param("clientId")
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
err := oc.oidcService.RevokeAuthorizedClient(c.Request.Context(), userID, clientID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// listOwnAccessibleClientsHandler godoc
|
||||
// @Summary List accessible OIDC clients for current user
|
||||
// @Description Get a list of OIDC clients that the current user can access
|
||||
// @Tags OIDC
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @Param sort[column] query string false "Column to sort by"
|
||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
|
||||
// @Router /api/oidc/users/me/clients [get]
|
||||
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, sortedPaginationRequest)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.AccessibleOidcClientDto]{
|
||||
Data: clients,
|
||||
Pagination: pagination,
|
||||
})
|
||||
}
|
||||
|
||||
func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) {
|
||||
userCode := c.Query("code")
|
||||
if userCode == "" {
|
||||
|
||||
@@ -14,6 +14,11 @@ import (
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultOneTimeAccessTokenDuration = 15 * time.Minute
|
||||
defaultSignupTokenDuration = time.Hour
|
||||
)
|
||||
|
||||
// NewUserController creates a new controller for user management endpoints
|
||||
// @Summary User management controller
|
||||
// @Description Initializes all user-related API endpoints
|
||||
@@ -331,10 +336,17 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bo
|
||||
return
|
||||
}
|
||||
|
||||
var ttl time.Duration
|
||||
if own {
|
||||
input.UserID = c.GetString("userID")
|
||||
ttl = defaultOneTimeAccessTokenDuration
|
||||
} else {
|
||||
ttl = input.TTL.Duration
|
||||
if ttl <= 0 {
|
||||
ttl = defaultOneTimeAccessTokenDuration
|
||||
}
|
||||
}
|
||||
token, err := uc.userService.CreateOneTimeAccessToken(c.Request.Context(), input.UserID, input.ExpiresAt)
|
||||
token, err := uc.userService.CreateOneTimeAccessToken(c.Request.Context(), input.UserID, ttl)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -411,7 +423,11 @@ func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context
|
||||
|
||||
userID := c.Param("id")
|
||||
|
||||
err := uc.userService.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), userID, input.ExpiresAt)
|
||||
ttl := input.TTL.Duration
|
||||
if ttl <= 0 {
|
||||
ttl = defaultOneTimeAccessTokenDuration
|
||||
}
|
||||
err := uc.userService.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), userID, ttl)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -526,14 +542,20 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
signupToken, err := uc.userService.CreateSignupToken(c.Request.Context(), input.ExpiresAt, input.UsageLimit)
|
||||
ttl := input.TTL.Duration
|
||||
if ttl <= 0 {
|
||||
ttl = defaultSignupTokenDuration
|
||||
}
|
||||
|
||||
signupToken, err := uc.userService.CreateSignupToken(c.Request.Context(), ttl, input.UsageLimit)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var tokenDto dto.SignupTokenDto
|
||||
if err := dto.MapStruct(signupToken, &tokenDto); err != nil {
|
||||
err = dto.MapStruct(signupToken, &tokenDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ func NewWebauthnController(group *gin.RouterGroup, authMiddleware *middleware.Au
|
||||
|
||||
group.POST("/webauthn/logout", authMiddleware.WithAdminNotRequired().Add(), wc.logoutHandler)
|
||||
|
||||
group.POST("/webauthn/reauthenticate", authMiddleware.WithAdminNotRequired().Add(), rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), wc.reauthenticateHandler)
|
||||
|
||||
group.GET("/webauthn/credentials", authMiddleware.WithAdminNotRequired().Add(), wc.listCredentialsHandler)
|
||||
group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler)
|
||||
group.DELETE("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.deleteCredentialHandler)
|
||||
@@ -171,3 +173,33 @@ func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
||||
cookie.AddAccessTokenCookie(c, 0, "")
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (wc *WebauthnController) reauthenticateHandler(c *gin.Context) {
|
||||
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
|
||||
if err != nil {
|
||||
_ = c.Error(&common.MissingSessionIdError{})
|
||||
return
|
||||
}
|
||||
|
||||
var token string
|
||||
|
||||
// Try to create a reauthentication token with WebAuthn
|
||||
credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body)
|
||||
if err == nil {
|
||||
token, err = wc.webAuthnService.CreateReauthenticationTokenWithWebauthn(c.Request.Context(), sessionID, credentialAssertionData)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If WebAuthn fails, try to create a reauthentication token with the access token
|
||||
accessToken, _ := c.Cookie(cookie.AccessTokenCookieName)
|
||||
token, err = wc.webAuthnService.CreateReauthenticationTokenWithAccessToken(c.Request.Context(), accessToken)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"reauthenticationToken": token})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -23,7 +24,9 @@ func NewWellKnownController(group *gin.RouterGroup, jwtService *service.JwtServi
|
||||
var err error
|
||||
wkc.oidcConfig, err = wkc.computeOIDCConfiguration()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to pre-compute OpenID Connect configuration document: %v", err)
|
||||
slog.Error("Failed to pre-compute OpenID Connect configuration document", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
group.GET("/.well-known/jwks.json", wkc.jwksHandler)
|
||||
@@ -84,6 +87,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
||||
"subject_types_supported": []string{"public"},
|
||||
"id_token_signing_alg_values_supported": []string{alg.String()},
|
||||
"authorization_response_iss_parameter_supported": true,
|
||||
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||
}
|
||||
return json.Marshal(config)
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
|
||||
type ApiKeyCreateDto struct {
|
||||
Name string `json:"name" binding:"required,min=3,max=50" unorm:"nfc"`
|
||||
Description string `json:"description" unorm:"nfc"`
|
||||
Description *string `json:"description" unorm:"nfc"`
|
||||
ExpiresAt datatype.DateTime `json:"expiresAt" binding:"required"`
|
||||
}
|
||||
|
||||
type ApiKeyDto struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Description *string `json:"description"`
|
||||
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
||||
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||
|
||||
@@ -18,6 +18,8 @@ type AppConfigUpdateDto struct {
|
||||
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||
AllowUserSignups string `json:"allowUserSignups" binding:"required,oneof=disabled withToken open"`
|
||||
SignupDefaultUserGroupIDs string `json:"signupDefaultUserGroupIDs" binding:"omitempty,json"`
|
||||
SignupDefaultCustomClaims string `json:"signupDefaultCustomClaims" binding:"omitempty,json"`
|
||||
AccentColor string `json:"accentColor"`
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort string `json:"smtpPort"`
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package dto
|
||||
|
||||
import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
|
||||
type OidcClientMetaDataDto struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
LaunchURL *string `json:"launchURL"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
}
|
||||
|
||||
type OidcClientDto struct {
|
||||
@@ -25,13 +29,20 @@ type OidcClientWithAllowedGroupsCountDto struct {
|
||||
AllowedUserGroupsCount int64 `json:"allowedUserGroupsCount"`
|
||||
}
|
||||
|
||||
type OidcClientUpdateDto struct {
|
||||
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
|
||||
CallbackURLs []string `json:"callbackURLs"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
||||
}
|
||||
|
||||
type OidcClientCreateDto struct {
|
||||
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
|
||||
CallbackURLs []string `json:"callbackURLs"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
OidcClientUpdateDto
|
||||
ID string `json:"id" binding:"omitempty,client_id,min=2,max=128"`
|
||||
}
|
||||
|
||||
type OidcClientCredentialsDto struct {
|
||||
@@ -46,12 +57,13 @@ type OidcClientFederatedIdentityDto struct {
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientRequestDto struct {
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
Nonce string `json:"nonce"`
|
||||
CodeChallenge string `json:"codeChallenge"`
|
||||
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
Nonce string `json:"nonce"`
|
||||
CodeChallenge string `json:"codeChallenge"`
|
||||
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
||||
ReauthenticationToken string `json:"reauthenticationToken"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientResponseDto struct {
|
||||
@@ -145,8 +157,9 @@ type DeviceCodeInfoDto struct {
|
||||
}
|
||||
|
||||
type AuthorizedOidcClientDto struct {
|
||||
Scope string `json:"scope"`
|
||||
Client OidcClientMetaDataDto `json:"client"`
|
||||
Scope string `json:"scope"`
|
||||
Client OidcClientMetaDataDto `json:"client"`
|
||||
LastUsedAt datatype.DateTime `json:"lastUsedAt"`
|
||||
}
|
||||
|
||||
type OidcClientPreviewDto struct {
|
||||
@@ -154,3 +167,8 @@ type OidcClientPreviewDto struct {
|
||||
AccessToken map[string]any `json:"accessToken"`
|
||||
UserInfo map[string]any `json:"userInfo"`
|
||||
}
|
||||
|
||||
type AccessibleOidcClientDto struct {
|
||||
OidcClientMetaDataDto
|
||||
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
type SignupTokenCreateDto struct {
|
||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
|
||||
TTL utils.JSONDuration `json:"ttl" binding:"required,ttl"`
|
||||
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type SignupTokenDto struct {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
type UserDto struct {
|
||||
@@ -30,8 +30,8 @@ type UserCreateDto struct {
|
||||
}
|
||||
|
||||
type OneTimeAccessTokenCreateDto struct {
|
||||
UserID string `json:"userId"`
|
||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||
UserID string `json:"userId"`
|
||||
TTL utils.JSONDuration `json:"ttl" binding:"ttl"`
|
||||
}
|
||||
|
||||
type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
||||
@@ -40,7 +40,7 @@ type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
||||
}
|
||||
|
||||
type OneTimeAccessEmailAsAdminDto struct {
|
||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||
TTL utils.JSONDuration `json:"ttl" binding:"ttl"`
|
||||
}
|
||||
|
||||
type UserUpdateUserGroupDto struct {
|
||||
|
||||
@@ -1,26 +1,52 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
||||
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
||||
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
|
||||
var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.@-]*[a-zA-Z0-9]$")
|
||||
|
||||
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
||||
return validateUsernameRegex.MatchString(fl.Field().String())
|
||||
}
|
||||
|
||||
func init() {
|
||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||
if err := v.RegisterValidation("username", validateUsername); err != nil {
|
||||
log.Fatalf("Failed to register custom validation: %v", err)
|
||||
v := binding.Validator.Engine().(*validator.Validate)
|
||||
|
||||
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
||||
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
||||
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
|
||||
var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.@-]*[a-zA-Z0-9]$")
|
||||
|
||||
var validateClientIDRegex = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
|
||||
|
||||
// Maximum allowed value for TTLs
|
||||
const maxTTL = 31 * 24 * time.Hour
|
||||
|
||||
// Errors here are development-time ones
|
||||
err := v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
|
||||
return validateUsernameRegex.MatchString(fl.Field().String())
|
||||
})
|
||||
if err != nil {
|
||||
panic("Failed to register custom validation for username: " + err.Error())
|
||||
}
|
||||
|
||||
err = v.RegisterValidation("client_id", func(fl validator.FieldLevel) bool {
|
||||
return validateClientIDRegex.MatchString(fl.Field().String())
|
||||
})
|
||||
if err != nil {
|
||||
panic("Failed to register custom validation for client_id: " + err.Error())
|
||||
}
|
||||
|
||||
err = v.RegisterValidation("ttl", func(fl validator.FieldLevel) bool {
|
||||
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Allow zero, which means the field wasn't set
|
||||
return ttl.Duration == 0 || ttl.Duration > time.Second && ttl.Duration <= maxTTL
|
||||
})
|
||||
if err != nil {
|
||||
panic("Failed to register custom validation for ttl: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) erro
|
||||
s.registerJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true),
|
||||
s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true),
|
||||
s.registerJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true),
|
||||
s.registerJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true),
|
||||
s.registerJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true),
|
||||
)
|
||||
}
|
||||
@@ -104,6 +105,20 @@ func (j *DbCleanupJobs) clearOidcRefreshTokens(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearReauthenticationTokens deletes reauthentication tokens that have expired
|
||||
func (j *DbCleanupJobs) clearReauthenticationTokens(ctx context.Context) error {
|
||||
st := j.db.
|
||||
WithContext(ctx).
|
||||
Delete(&model.ReauthenticationToken{}, "expires_at < ?", datatype.DateTime(time.Now()))
|
||||
if st.Error != nil {
|
||||
return fmt.Errorf("failed to clean expired reauthentication tokens: %w", st.Error)
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "Cleaned expired reauthentication tokens", slog.Int64("count", st.RowsAffected))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearAuditLogs deletes audit logs older than 90 days
|
||||
func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
|
||||
st := j.db.
|
||||
|
||||
@@ -34,13 +34,15 @@ func (a *AppConfigVariable) AsDurationMinutes() time.Duration {
|
||||
|
||||
type AppConfig struct {
|
||||
// General
|
||||
AppName AppConfigVariable `key:"appName,public"` // Public
|
||||
SessionDuration AppConfigVariable `key:"sessionDuration"`
|
||||
EmailsVerified AppConfigVariable `key:"emailsVerified"`
|
||||
AccentColor AppConfigVariable `key:"accentColor,public"` // Public
|
||||
DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public
|
||||
AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
|
||||
AllowUserSignups AppConfigVariable `key:"allowUserSignups,public"` // Public
|
||||
AppName AppConfigVariable `key:"appName,public"` // Public
|
||||
SessionDuration AppConfigVariable `key:"sessionDuration"`
|
||||
EmailsVerified AppConfigVariable `key:"emailsVerified"`
|
||||
AccentColor AppConfigVariable `key:"accentColor,public"` // Public
|
||||
DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public
|
||||
AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
|
||||
AllowUserSignups AppConfigVariable `key:"allowUserSignups,public"` // Public
|
||||
SignupDefaultUserGroupIDs AppConfigVariable `key:"signupDefaultUserGroupIDs"`
|
||||
SignupDefaultCustomClaims AppConfigVariable `key:"signupDefaultCustomClaims"`
|
||||
// Internal
|
||||
BackgroundImageType AppConfigVariable `key:"backgroundImageType,internal"` // Internal
|
||||
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
||||
@@ -178,7 +180,7 @@ type AppConfigKeyNotFoundError struct {
|
||||
}
|
||||
|
||||
func (e AppConfigKeyNotFoundError) Error() string {
|
||||
return fmt.Sprintf("cannot find config key '%s'", e.field)
|
||||
return "cannot find config key '" + e.field + "'"
|
||||
}
|
||||
|
||||
func (e AppConfigKeyNotFoundError) Is(target error) bool {
|
||||
@@ -192,7 +194,7 @@ type AppConfigInternalForbiddenError struct {
|
||||
}
|
||||
|
||||
func (e AppConfigInternalForbiddenError) Error() string {
|
||||
return fmt.Sprintf("field '%s' is internal and can't be updated", e.field)
|
||||
return "field '" + e.field + "' is internal and can't be updated"
|
||||
}
|
||||
|
||||
func (e AppConfigInternalForbiddenError) Is(target error) bool {
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
)
|
||||
|
||||
type UserAuthorizedOidcClient struct {
|
||||
Scope string
|
||||
Scope string
|
||||
LastUsedAt datatype.DateTime `sortable:"true"`
|
||||
|
||||
UserID string `gorm:"primary_key;"`
|
||||
User User
|
||||
|
||||
@@ -38,19 +40,22 @@ type OidcAuthorizationCode struct {
|
||||
type OidcClient struct {
|
||||
Base
|
||||
|
||||
Name string `sortable:"true"`
|
||||
Secret string
|
||||
CallbackURLs UrlList
|
||||
LogoutCallbackURLs UrlList
|
||||
ImageType *string
|
||||
HasLogo bool `gorm:"-"`
|
||||
IsPublic bool
|
||||
PkceEnabled bool
|
||||
Credentials OidcClientCredentials
|
||||
Name string `sortable:"true"`
|
||||
Secret string
|
||||
CallbackURLs UrlList
|
||||
LogoutCallbackURLs UrlList
|
||||
ImageType *string
|
||||
HasLogo bool `gorm:"-"`
|
||||
IsPublic bool
|
||||
PkceEnabled bool
|
||||
RequiresReauthentication bool
|
||||
Credentials OidcClientCredentials
|
||||
LaunchURL *string
|
||||
|
||||
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||
CreatedByID string
|
||||
CreatedBy User
|
||||
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||
CreatedByID *string
|
||||
CreatedBy *User
|
||||
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
|
||||
}
|
||||
|
||||
type OidcRefreshToken struct {
|
||||
|
||||
@@ -45,6 +45,15 @@ type PublicKeyCredentialRequestOptions struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type ReauthenticationToken struct {
|
||||
Base
|
||||
Token string
|
||||
ExpiresAt datatype.DateTime
|
||||
|
||||
UserID string
|
||||
User User
|
||||
}
|
||||
|
||||
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
|
||||
|
||||
// Scan and Value methods for GORM to handle the custom type
|
||||
|
||||
@@ -55,8 +55,8 @@ func (s *ApiKeyService) CreateApiKey(ctx context.Context, userID string, input d
|
||||
apiKey := model.ApiKey{
|
||||
Name: input.Name,
|
||||
Key: utils.CreateSha256Hash(token), // Hash the token for storage
|
||||
Description: &input.Description,
|
||||
ExpiresAt: datatype.DateTime(input.ExpiresAt),
|
||||
Description: input.Description,
|
||||
ExpiresAt: input.ExpiresAt,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
@@ -29,22 +26,22 @@ type AppConfigService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAppConfigService(ctx context.Context, db *gorm.DB) *AppConfigService {
|
||||
func NewAppConfigService(ctx context.Context, db *gorm.DB) (*AppConfigService, error) {
|
||||
service := &AppConfigService{
|
||||
db: db,
|
||||
}
|
||||
|
||||
err := service.LoadDbConfig(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize app config service: %v", err)
|
||||
return nil, fmt.Errorf("failed to initialize app config service: %w", err)
|
||||
}
|
||||
|
||||
err = service.initInstanceID(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize instance ID: %v", err)
|
||||
return nil, fmt.Errorf("failed to initialize instance ID: %w", err)
|
||||
}
|
||||
|
||||
return service
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// GetDbConfig returns the application configuration.
|
||||
@@ -63,13 +60,15 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
||||
// Values are the default ones
|
||||
return &model.AppConfig{
|
||||
// General
|
||||
AppName: model.AppConfigVariable{Value: "Pocket ID"},
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
EmailsVerified: model.AppConfigVariable{Value: "false"},
|
||||
DisableAnimations: model.AppConfigVariable{Value: "false"},
|
||||
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
|
||||
AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
|
||||
AccentColor: model.AppConfigVariable{Value: "default"},
|
||||
AppName: model.AppConfigVariable{Value: "Pocket ID"},
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
EmailsVerified: model.AppConfigVariable{Value: "false"},
|
||||
DisableAnimations: model.AppConfigVariable{Value: "false"},
|
||||
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
|
||||
AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
|
||||
SignupDefaultUserGroupIDs: model.AppConfigVariable{Value: "[]"},
|
||||
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
|
||||
AccentColor: model.AppConfigVariable{Value: "default"},
|
||||
// Internal
|
||||
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
|
||||
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
||||
@@ -414,12 +413,10 @@ func (s *AppConfigService) loadDbConfigFromEnv(ctx context.Context, tx *gorm.DB)
|
||||
field := rt.Field(i)
|
||||
|
||||
// Get the key and internal tag values
|
||||
tagValue := strings.Split(field.Tag.Get("key"), ",")
|
||||
key := tagValue[0]
|
||||
isInternal := slices.Contains(tagValue, "internal")
|
||||
key, attrs, _ := strings.Cut(field.Tag.Get("key"), ",")
|
||||
|
||||
// Internal fields are loaded from the database as they can't be set from the environment
|
||||
if isInternal {
|
||||
if attrs == "internal" {
|
||||
var value string
|
||||
err := tx.WithContext(ctx).
|
||||
Model(&model.AppConfigVariable{}).
|
||||
@@ -438,6 +435,20 @@ func (s *AppConfigService) loadDbConfigFromEnv(ctx context.Context, tx *gorm.DB)
|
||||
value, ok := os.LookupEnv(envVarName)
|
||||
if ok {
|
||||
rv.Field(i).FieldByName("Value").SetString(value)
|
||||
continue
|
||||
}
|
||||
|
||||
// If it's sensitive, we also allow reading from file
|
||||
if attrs == "sensitive" {
|
||||
fileName := os.Getenv(envVarName + "_FILE")
|
||||
if fileName != "" {
|
||||
b, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret '%s' from file '%s': %w", envVarName, fileName, err)
|
||||
}
|
||||
rv.Field(i).FieldByName("Value").SetString(string(b))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
|
||||
userAgentParser "github.com/mileusna/useragent"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,12 @@ type AuditLogService struct {
|
||||
}
|
||||
|
||||
func NewAuditLogService(db *gorm.DB, appConfigService *AppConfigService, emailService *EmailService, geoliteService *GeoLiteService) *AuditLogService {
|
||||
return &AuditLogService{db: db, appConfigService: appConfigService, emailService: emailService, geoliteService: geoliteService}
|
||||
return &AuditLogService{
|
||||
db: db,
|
||||
appConfigService: appConfigService,
|
||||
emailService: emailService,
|
||||
geoliteService: geoliteService,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new audit log entry in the database
|
||||
@@ -82,7 +87,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
}
|
||||
err := stmt.Count(&count).Error
|
||||
if err != nil {
|
||||
log.Printf("Failed to count audit logs: %v", err)
|
||||
slog.ErrorContext(ctx, "Failed to count audit logs", slog.Any("error", err))
|
||||
return createdAuditLog
|
||||
}
|
||||
|
||||
@@ -91,7 +96,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
// We use a background context here as this is running in a goroutine
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
innerCtx := context.Background()
|
||||
span := trace.SpanFromContext(ctx)
|
||||
innerCtx := trace.ContextWithSpan(context.Background(), span)
|
||||
|
||||
// Note we don't use the transaction here because this is running in background
|
||||
var user model.User
|
||||
@@ -101,7 +107,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
First(&user).
|
||||
Error
|
||||
if innerErr != nil {
|
||||
log.Printf("Failed to load user: %v", innerErr)
|
||||
slog.ErrorContext(innerCtx, "Failed to load user from database to send notification email", slog.Any("error", innerErr))
|
||||
return
|
||||
}
|
||||
|
||||
innerErr = SendEmail(innerCtx, s.emailService, email.Address{
|
||||
@@ -115,7 +122,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
DateTime: createdAuditLog.CreatedAt.UTC(),
|
||||
})
|
||||
if innerErr != nil {
|
||||
log.Printf("Failed to send email to '%s': %v", user.Email, innerErr)
|
||||
slog.ErrorContext(innerCtx, "Failed to send notification email", slog.Any("error", innerErr), slog.String("address", user.Email))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -55,16 +55,46 @@ const (
|
||||
|
||||
// UpdateCustomClaimsForUser updates the custom claims for a user
|
||||
func (s *CustomClaimService) UpdateCustomClaimsForUser(ctx context.Context, userID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
||||
return s.updateCustomClaims(ctx, UserID, userID, claims)
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
updatedClaims, err := s.updateCustomClaimsInternal(ctx, UserID, userID, claims, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedClaims, nil
|
||||
}
|
||||
|
||||
// UpdateCustomClaimsForUserGroup updates the custom claims for a user group
|
||||
func (s *CustomClaimService) UpdateCustomClaimsForUserGroup(ctx context.Context, userGroupID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
||||
return s.updateCustomClaims(ctx, UserGroupID, userGroupID, claims)
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
updatedClaims, err := s.updateCustomClaimsInternal(ctx, UserGroupID, userGroupID, claims, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedClaims, nil
|
||||
}
|
||||
|
||||
// updateCustomClaims updates the custom claims for a user or user group
|
||||
func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idType, value string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
||||
// updateCustomClaimsInternal updates the custom claims for a user or user group within a transaction
|
||||
func (s *CustomClaimService) updateCustomClaimsInternal(ctx context.Context, idType idType, value string, claims []dto.CustomClaimCreateDto, tx *gorm.DB) ([]model.CustomClaim, error) {
|
||||
// Check for duplicate keys in the claims slice
|
||||
seenKeys := make(map[string]struct{})
|
||||
for _, claim := range claims {
|
||||
@@ -74,11 +104,6 @@ func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idTy
|
||||
seenKeys[claim.Key] = struct{}{}
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var existingClaims []model.CustomClaim
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
@@ -150,11 +175,6 @@ func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idTy
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedClaims, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -154,11 +154,12 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
ID: "3654a746-35d4-4321-ac61-0bdcff2b4055",
|
||||
},
|
||||
Name: "Nextcloud",
|
||||
LaunchURL: utils.Ptr("https://nextcloud.local"),
|
||||
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
|
||||
CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"},
|
||||
LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"},
|
||||
ImageType: utils.StringPointer("png"),
|
||||
CreatedByID: users[0].ID,
|
||||
CreatedByID: utils.Ptr(users[0].ID),
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
@@ -167,11 +168,21 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
Name: "Immich",
|
||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
|
||||
CreatedByID: users[1].ID,
|
||||
CreatedByID: utils.Ptr(users[1].ID),
|
||||
AllowedUserGroups: []model.UserGroup{
|
||||
userGroups[1],
|
||||
},
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "7c21a609-96b5-4011-9900-272b8d31a9d1",
|
||||
},
|
||||
Name: "Tailscale",
|
||||
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
|
||||
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
|
||||
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
|
||||
CreatedByID: utils.Ptr(users[0].ID),
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "c48232ff-ff65-45ed-ae96-7afa8a9b443b",
|
||||
@@ -179,7 +190,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
Name: "Federated",
|
||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||
CallbackURLs: model.UrlList{"http://federated/auth/callback"},
|
||||
CreatedByID: users[1].ID,
|
||||
CreatedByID: utils.Ptr(users[1].ID),
|
||||
AllowedUserGroups: []model.UserGroup{},
|
||||
Credentials: model.OidcClientCredentials{
|
||||
FederatedIdentities: []model.OidcClientFederatedIdentity{
|
||||
@@ -245,14 +256,22 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
|
||||
userAuthorizedClients := []model.UserAuthorizedOidcClient{
|
||||
{
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
LastUsedAt: datatype.DateTime(time.Date(2025, 8, 1, 13, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
{
|
||||
Scope: "openid profile email",
|
||||
UserID: users[1].ID,
|
||||
ClientID: oidcClients[2].ID,
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[2].ID,
|
||||
LastUsedAt: datatype.DateTime(time.Date(2025, 8, 10, 14, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
{
|
||||
Scope: "openid profile email",
|
||||
UserID: users[1].ID,
|
||||
ClientID: oidcClients[3].ID,
|
||||
LastUsedAt: datatype.DateTime(time.Date(2025, 8, 12, 12, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}
|
||||
for _, userAuthorizedClient := range userAuthorizedClients {
|
||||
@@ -324,7 +343,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "b2c3d4e5-f6g7-8901-bcde-f12345678901",
|
||||
ID: "dc3c9c96-714e-48eb-926e-2d7c7858e6cf",
|
||||
},
|
||||
Token: "PARTIAL567890ABC",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(7 * 24 * time.Hour)),
|
||||
@@ -333,7 +352,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "c3d4e5f6-g7h8-9012-cdef-123456789012",
|
||||
ID: "44de1863-ffa5-4db1-9507-4887cd7a1e3f",
|
||||
},
|
||||
Token: "EXPIRED34567890B",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(-24 * time.Hour)), // Expired
|
||||
@@ -342,7 +361,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "d4e5f6g7-h8i9-0123-def0-234567890123",
|
||||
ID: "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b",
|
||||
},
|
||||
Token: "FULLYUSED567890C",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||
@@ -402,9 +421,9 @@ func (s *TestService) ResetDatabase() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TestService) ResetApplicationImages() error {
|
||||
func (s *TestService) ResetApplicationImages(ctx context.Context) error {
|
||||
if err := os.RemoveAll(common.EnvConfig.UploadPath); err != nil {
|
||||
log.Printf("Error removing directory: %v", err)
|
||||
slog.ErrorContext(ctx, "Error removing directory", slog.Any("error", err))
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -52,13 +52,14 @@ func NewGeoLiteService(httpClient *http.Client) *GeoLiteService {
|
||||
|
||||
if common.EnvConfig.MaxMindLicenseKey == "" && common.EnvConfig.GeoLiteDBUrl == common.MaxMindGeoLiteCityUrl {
|
||||
// Warn the user, and disable the periodic updater
|
||||
log.Println("MAXMIND_LICENSE_KEY environment variable is empty. The GeoLite2 City database won't be updated.")
|
||||
slog.Warn("MAXMIND_LICENSE_KEY environment variable is empty: the GeoLite2 City database won't be updated")
|
||||
service.disableUpdater = true
|
||||
}
|
||||
|
||||
// Initialize IPv6 local ranges
|
||||
if err := service.initializeIPv6LocalRanges(); err != nil {
|
||||
log.Printf("Warning: Failed to initialize IPv6 local ranges: %v", err)
|
||||
err := service.initializeIPv6LocalRanges()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to initialize IPv6 local ranges", slog.Any("error", err))
|
||||
}
|
||||
|
||||
return service
|
||||
@@ -96,7 +97,7 @@ func (s *GeoLiteService) initializeIPv6LocalRanges() error {
|
||||
s.localIPv6Ranges = localRanges
|
||||
|
||||
if len(localRanges) > 0 {
|
||||
log.Printf("Initialized %d IPv6 local ranges", len(localRanges))
|
||||
slog.Info("Initialized IPv6 local ranges", slog.Int("count", len(localRanges)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -186,11 +187,11 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
|
||||
// UpdateDatabase checks the age of the database and updates it if it's older than 14 days.
|
||||
func (s *GeoLiteService) UpdateDatabase(parentCtx context.Context) error {
|
||||
if s.isDatabaseUpToDate() {
|
||||
log.Println("GeoLite2 City database is up-to-date")
|
||||
slog.Info("GeoLite2 City database is up-to-date")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Updating GeoLite2 City database")
|
||||
slog.Info("Updating GeoLite2 City database")
|
||||
downloadUrl := fmt.Sprintf(common.EnvConfig.GeoLiteDBUrl, common.EnvConfig.MaxMindLicenseKey)
|
||||
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Minute)
|
||||
@@ -217,7 +218,7 @@ func (s *GeoLiteService) UpdateDatabase(parentCtx context.Context) error {
|
||||
return fmt.Errorf("failed to extract database: %w", err)
|
||||
}
|
||||
|
||||
log.Println("GeoLite2 City database successfully updated.")
|
||||
slog.Info("GeoLite2 City database successfully updated.")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
@@ -64,16 +63,16 @@ type JwtService struct {
|
||||
jwksEncoded []byte
|
||||
}
|
||||
|
||||
func NewJwtService(db *gorm.DB, appConfigService *AppConfigService) *JwtService {
|
||||
func NewJwtService(db *gorm.DB, appConfigService *AppConfigService) (*JwtService, error) {
|
||||
service := &JwtService{}
|
||||
|
||||
// Ensure keys are generated or loaded
|
||||
err := service.init(db, appConfigService, &common.EnvConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize jwt service: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return service
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *JwtService) init(db *gorm.DB, appConfigService *AppConfigService, envConfig *common.EnvConfigSchema) (err error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -130,7 +130,7 @@ func (s *LdapService) SyncGroups(ctx context.Context, tx *gorm.DB, client *ldap.
|
||||
|
||||
// Skip groups without a valid LDAP ID
|
||||
if ldapId == "" {
|
||||
log.Printf("Skipping LDAP group without a valid unique identifier (attribute: %s)", dbConfig.LdapAttributeGroupUniqueIdentifier.Value)
|
||||
slog.Warn("Skipping LDAP group without a valid unique identifier", slog.String("attribute", dbConfig.LdapAttributeGroupUniqueIdentifier.Value))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -168,13 +168,13 @@ func (s *LdapService) SyncGroups(ctx context.Context, tx *gorm.DB, client *ldap.
|
||||
|
||||
userResult, err := client.Search(userSearchReq)
|
||||
if err != nil || len(userResult.Entries) == 0 {
|
||||
log.Printf("Could not resolve group member DN '%s': %v", member, err)
|
||||
slog.WarnContext(ctx, "Could not resolve group member DN", slog.String("member", member), slog.Any("error", err))
|
||||
continue
|
||||
}
|
||||
|
||||
username = userResult.Entries[0].GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value)
|
||||
if username == "" {
|
||||
log.Printf("Could not extract username from group member DN '%s'", member)
|
||||
slog.WarnContext(ctx, "Could not extract username from group member DN", slog.String("member", member))
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func (s *LdapService) SyncGroups(ctx context.Context, tx *gorm.DB, client *ldap.
|
||||
return fmt.Errorf("failed to delete group '%s': %w", group.Name, err)
|
||||
}
|
||||
|
||||
log.Printf("Deleted group '%s'", group.Name)
|
||||
slog.Info("Deleted group", slog.String("group", group.Name))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -295,7 +295,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
||||
|
||||
// Skip users without a valid LDAP ID
|
||||
if ldapId == "" {
|
||||
log.Printf("Skipping LDAP user without a valid unique identifier (attribute: %s)", dbConfig.LdapAttributeUserUniqueIdentifier.Value)
|
||||
slog.Warn("Skipping LDAP user without a valid unique identifier", slog.String("attribute", dbConfig.LdapAttributeUserUniqueIdentifier.Value))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
||||
if databaseUser.ID == "" {
|
||||
_, err = s.userService.createUserInternal(ctx, newUser, true, tx)
|
||||
if errors.Is(err, &common.AlreadyInUseError{}) {
|
||||
log.Printf("Skipping creating LDAP user '%s': %v", newUser.Username, err)
|
||||
slog.Warn("Skipping creating LDAP user", slog.String("username", newUser.Username), slog.Any("error", err))
|
||||
continue
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error creating user '%s': %w", newUser.Username, err)
|
||||
@@ -358,7 +358,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
||||
} else {
|
||||
_, err = s.userService.updateUserInternal(ctx, databaseUser.ID, newUser, false, true, tx)
|
||||
if errors.Is(err, &common.AlreadyInUseError{}) {
|
||||
log.Printf("Skipping updating LDAP user '%s': %v", newUser.Username, err)
|
||||
slog.Warn("Skipping updating LDAP user", slog.String("username", newUser.Username), slog.Any("error", err))
|
||||
continue
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error updating user '%s': %w", newUser.Username, err)
|
||||
@@ -371,7 +371,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
||||
err = s.saveProfilePicture(ctx, databaseUser.ID, pictureString)
|
||||
if err != nil {
|
||||
// This is not a fatal error
|
||||
log.Printf("Error saving profile picture for user %s: %v", newUser.Username, err)
|
||||
slog.Warn("Error saving profile picture for user", slog.String("username", newUser.Username), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +400,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
||||
return fmt.Errorf("failed to disable user %s: %w", user.Username, err)
|
||||
}
|
||||
|
||||
log.Printf("Disabled user '%s'", user.Username)
|
||||
slog.Info("Disabled user", slog.String("username", user.Username))
|
||||
} else {
|
||||
err = s.userService.deleteUserInternal(ctx, user.ID, true, tx)
|
||||
target := &common.LdapUserUpdateError{}
|
||||
@@ -410,7 +410,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
||||
return fmt.Errorf("failed to delete user %s: %w", user.Username, err)
|
||||
}
|
||||
|
||||
log.Printf("Deleted user '%s'", user.Username)
|
||||
slog.Info("Deleted user", slog.String("username", user.Username))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -51,6 +50,7 @@ type OidcService struct {
|
||||
appConfigService *AppConfigService
|
||||
auditLogService *AuditLogService
|
||||
customClaimService *CustomClaimService
|
||||
webAuthnService *WebAuthnService
|
||||
|
||||
httpClient *http.Client
|
||||
jwkCache *jwk.Cache
|
||||
@@ -63,6 +63,7 @@ func NewOidcService(
|
||||
appConfigService *AppConfigService,
|
||||
auditLogService *AuditLogService,
|
||||
customClaimService *CustomClaimService,
|
||||
webAuthnService *WebAuthnService,
|
||||
) (s *OidcService, err error) {
|
||||
s = &OidcService{
|
||||
db: db,
|
||||
@@ -70,6 +71,7 @@ func NewOidcService(
|
||||
appConfigService: appConfigService,
|
||||
auditLogService: auditLogService,
|
||||
customClaimService: customClaimService,
|
||||
webAuthnService: webAuthnService,
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -124,6 +126,16 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if client.RequiresReauthentication {
|
||||
if input.ReauthenticationToken == "" {
|
||||
return "", "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
err = s.webAuthnService.ConsumeReauthenticationToken(ctx, tx, input.ReauthenticationToken, userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// If the client is not public, the code challenge must be provided
|
||||
if client.IsPublic && input.CodeChallenge == "" {
|
||||
return "", "", &common.OidcMissingCodeChallengeError{}
|
||||
@@ -150,20 +162,11 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", &common.OidcAccessDeniedError{}
|
||||
}
|
||||
|
||||
// Check if the user has already authorized the client with the given scope
|
||||
hasAuthorizedClient, err := s.hasAuthorizedClientInternal(ctx, input.ClientID, userID, input.Scope, tx)
|
||||
hasAlreadyAuthorizedClient, err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// If the user has not authorized the client, create a new authorization in the database
|
||||
if !hasAuthorizedClient {
|
||||
err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Create the authorization code
|
||||
code, err := s.createAuthorizationCode(ctx, input.ClientID, userID, input.Scope, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod, tx)
|
||||
if err != nil {
|
||||
@@ -171,7 +174,7 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
}
|
||||
|
||||
// Log the authorization event
|
||||
if hasAuthorizedClient {
|
||||
if hasAlreadyAuthorizedClient {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx)
|
||||
} else {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventNewClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx)
|
||||
@@ -651,8 +654,7 @@ func (s *OidcService) ListClients(ctx context.Context, name string, sortedPagina
|
||||
}
|
||||
|
||||
// As allowedUserGroupsCount is not a column, we need to manually sort it
|
||||
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc"
|
||||
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && isValidSortDirection {
|
||||
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||
query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
|
||||
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
||||
Group("oidc_clients.id").
|
||||
@@ -668,22 +670,28 @@ func (s *OidcService) ListClients(ctx context.Context, name string, sortedPagina
|
||||
|
||||
func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) {
|
||||
client := model.OidcClient{
|
||||
CreatedByID: userID,
|
||||
Base: model.Base{
|
||||
ID: input.ID,
|
||||
},
|
||||
CreatedByID: utils.Ptr(userID),
|
||||
}
|
||||
updateOIDCClientModelFromDto(&client, &input)
|
||||
updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)
|
||||
|
||||
err := s.db.
|
||||
WithContext(ctx).
|
||||
Create(&client).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return model.OidcClient{}, &common.ClientIdAlreadyExistsError{}
|
||||
}
|
||||
return model.OidcClient{}, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientCreateDto) (model.OidcClient, error) {
|
||||
func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientUpdateDto) (model.OidcClient, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -717,7 +725,7 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientCreateDto) {
|
||||
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
|
||||
// Base fields
|
||||
client.Name = input.Name
|
||||
client.CallbackURLs = input.CallbackURLs
|
||||
@@ -725,19 +733,20 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
|
||||
client.IsPublic = input.IsPublic
|
||||
// PKCE is required for public clients
|
||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||
client.RequiresReauthentication = input.RequiresReauthentication
|
||||
client.LaunchURL = input.LaunchURL
|
||||
|
||||
// Credentials
|
||||
if len(input.Credentials.FederatedIdentities) > 0 {
|
||||
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
||||
for i, fi := range input.Credentials.FederatedIdentities {
|
||||
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
|
||||
Issuer: fi.Issuer,
|
||||
Audience: fi.Audience,
|
||||
Subject: fi.Subject,
|
||||
JWKS: fi.JWKS,
|
||||
}
|
||||
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
||||
for i, fi := range input.Credentials.FederatedIdentities {
|
||||
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
|
||||
Issuer: fi.Issuer,
|
||||
Audience: fi.Audience,
|
||||
Subject: fi.Subject,
|
||||
JWKS: fi.JWKS,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error {
|
||||
@@ -1180,9 +1189,13 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
|
||||
}()
|
||||
|
||||
var deviceAuth model.OidcDeviceCode
|
||||
if err := tx.WithContext(ctx).Preload("Client.AllowedUserGroups").First(&deviceAuth, "user_code = ?", userCode).Error; err != nil {
|
||||
log.Printf("Error finding device code with user_code %s: %v", userCode, err)
|
||||
return err
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Preload("Client.AllowedUserGroups").
|
||||
First(&deviceAuth, "user_code = ?", userCode).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding device code: %w", err)
|
||||
}
|
||||
|
||||
if time.Now().After(deviceAuth.ExpiresAt.ToTime()) {
|
||||
@@ -1191,17 +1204,26 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
|
||||
|
||||
// Check if the user group is allowed to authorize the client
|
||||
var user model.User
|
||||
if err := tx.WithContext(ctx).Preload("UserGroups").First(&user, "id = ?", userID).Error; err != nil {
|
||||
return err
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Preload("UserGroups").
|
||||
First(&user, "id = ?", userID).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding user groups: %w", err)
|
||||
}
|
||||
|
||||
if !s.IsUserGroupAllowedToAuthorize(user, deviceAuth.Client) {
|
||||
return &common.OidcAccessDeniedError{}
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Preload("Client").First(&deviceAuth, "user_code = ?", userCode).Error; err != nil {
|
||||
log.Printf("Error finding device code with user_code %s: %v", userCode, err)
|
||||
return err
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Preload("Client").
|
||||
First(&deviceAuth, "user_code = ?", userCode).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding device code: %w", err)
|
||||
}
|
||||
|
||||
if time.Now().After(deviceAuth.ExpiresAt.ToTime()) {
|
||||
@@ -1211,33 +1233,24 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
|
||||
deviceAuth.UserID = &userID
|
||||
deviceAuth.IsAuthorized = true
|
||||
|
||||
if err := tx.WithContext(ctx).Save(&deviceAuth).Error; err != nil {
|
||||
log.Printf("Error saving device auth: %v", err)
|
||||
return err
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Save(&deviceAuth).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving device auth: %w", err)
|
||||
}
|
||||
|
||||
// Verify the update was successful
|
||||
var verifiedAuth model.OidcDeviceCode
|
||||
if err := tx.WithContext(ctx).First(&verifiedAuth, "device_code = ?", deviceAuth.DeviceCode).Error; err != nil {
|
||||
log.Printf("Error verifying update: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create user authorization if needed
|
||||
hasAuthorizedClient, err := s.hasAuthorizedClientInternal(ctx, deviceAuth.ClientID, userID, deviceAuth.Scope, tx)
|
||||
hasAlreadyAuthorizedClient, err := s.createAuthorizedClientInternal(ctx, userID, deviceAuth.ClientID, deviceAuth.Scope, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !hasAuthorizedClient {
|
||||
err := s.createAuthorizedClientInternal(ctx, userID, deviceAuth.ClientID, deviceAuth.Scope, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventNewDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
|
||||
auditLogData := model.AuditLogData{"clientName": deviceAuth.Client.Name}
|
||||
if hasAlreadyAuthorizedClient {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventDeviceCodeAuthorization, ipAddress, userAgent, userID, auditLogData, tx)
|
||||
} else {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventNewDeviceCodeAuthorization, ipAddress, userAgent, userID, auditLogData, tx)
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
@@ -1313,6 +1326,108 @@ func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string,
|
||||
return authorizedClients, response, err
|
||||
}
|
||||
|
||||
func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string, clientID string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var authorizedClient model.UserAuthorizedOidcClient
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ? AND client_id = ?", userID, clientID).
|
||||
First(&authorizedClient).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.WithContext(ctx).Delete(&authorizedClient).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var user model.User
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Preload("UserGroups").
|
||||
First(&user, "id = ?", userID).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, utils.PaginationResponse{}, err
|
||||
}
|
||||
|
||||
userGroupIDs := make([]string, len(user.UserGroups))
|
||||
for i, group := range user.UserGroups {
|
||||
userGroupIDs[i] = group.ID
|
||||
}
|
||||
|
||||
// Build the query for accessible clients
|
||||
query := tx.
|
||||
WithContext(ctx).
|
||||
Model(&model.OidcClient{}).
|
||||
Preload("UserAuthorizedOidcClients", "user_id = ?", userID)
|
||||
|
||||
// If user has no groups, only return clients with no allowed user groups
|
||||
if len(userGroupIDs) == 0 {
|
||||
query = query.
|
||||
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
||||
Where("oidc_clients_allowed_user_groups.oidc_client_id IS NULL")
|
||||
} else {
|
||||
// Return clients with no allowed user groups OR clients where user is in allowed groups
|
||||
query = query.
|
||||
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
||||
Where("oidc_clients_allowed_user_groups.oidc_client_id IS NULL OR oidc_clients_allowed_user_groups.user_group_id IN (?)", userGroupIDs)
|
||||
}
|
||||
|
||||
var clients []model.OidcClient
|
||||
|
||||
// Handle custom sorting for lastUsedAt column
|
||||
var response utils.PaginationResponse
|
||||
if sortedPaginationRequest.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||
query = query.
|
||||
Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
|
||||
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction + " NULLS LAST")
|
||||
}
|
||||
|
||||
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
|
||||
if err != nil {
|
||||
return nil, utils.PaginationResponse{}, err
|
||||
}
|
||||
|
||||
dtos := make([]dto.AccessibleOidcClientDto, len(clients))
|
||||
for i, client := range clients {
|
||||
var lastUsedAt *datatype.DateTime
|
||||
if len(client.UserAuthorizedOidcClients) > 0 {
|
||||
lastUsedAt = &client.UserAuthorizedOidcClients[0].LastUsedAt
|
||||
}
|
||||
dtos[i] = dto.AccessibleOidcClientDto{
|
||||
OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
|
||||
ID: client.ID,
|
||||
Name: client.Name,
|
||||
LaunchURL: client.LaunchURL,
|
||||
HasLogo: client.HasLogo,
|
||||
},
|
||||
LastUsedAt: lastUsedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return dtos, response, err
|
||||
}
|
||||
|
||||
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, tx *gorm.DB) (string, error) {
|
||||
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
|
||||
if err != nil {
|
||||
@@ -1348,14 +1463,37 @@ func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, u
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) createAuthorizedClientInternal(ctx context.Context, userID string, clientID string, scope string, tx *gorm.DB) error {
|
||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
Scope: scope,
|
||||
func (s *OidcService) createAuthorizedClientInternal(ctx context.Context, userID string, clientID string, scope string, tx *gorm.DB) (hasAlreadyAuthorizedClient bool, err error) {
|
||||
|
||||
// Check if the user has already authorized the client with the given scope
|
||||
hasAlreadyAuthorizedClient, err = s.hasAuthorizedClientInternal(ctx, clientID, userID, scope, tx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err := tx.WithContext(ctx).
|
||||
if hasAlreadyAuthorizedClient {
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Model(&model.UserAuthorizedOidcClient{}).
|
||||
Where("user_id = ? AND client_id = ?", userID, clientID).
|
||||
Update("last_used_at", datatype.DateTime(time.Now())).
|
||||
Error
|
||||
|
||||
if err != nil {
|
||||
return hasAlreadyAuthorizedClient, err
|
||||
}
|
||||
|
||||
return hasAlreadyAuthorizedClient, nil
|
||||
}
|
||||
|
||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
Scope: scope,
|
||||
LastUsedAt: datatype.DateTime(time.Now()),
|
||||
}
|
||||
|
||||
err = tx.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "client_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"scope"}),
|
||||
@@ -1363,7 +1501,7 @@ func (s *OidcService) createAuthorizedClientInternal(ctx context.Context, userID
|
||||
Create(&userAuthorizedClient).
|
||||
Error
|
||||
|
||||
return err
|
||||
return hasAlreadyAuthorizedClient, err
|
||||
}
|
||||
|
||||
type ClientAuthCredentials struct {
|
||||
@@ -1416,8 +1554,8 @@ func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *g
|
||||
|
||||
// Validate credentials based on the authentication method
|
||||
switch {
|
||||
// First, if we have a client secret, we validate it
|
||||
case input.ClientSecret != "":
|
||||
// First, if we have a client secret, we validate it unless client is marked as public
|
||||
case input.ClientSecret != "" && !client.IsPublic:
|
||||
err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(input.ClientSecret))
|
||||
if err != nil {
|
||||
return nil, &common.OidcClientSecretInvalidError{}
|
||||
@@ -1428,7 +1566,7 @@ func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *g
|
||||
case isClientAssertion:
|
||||
err = s.verifyClientAssertionFromFederatedIdentities(ctx, client, input)
|
||||
if err != nil {
|
||||
log.Printf("Invalid assertion for client '%s': %v", client.ID, err)
|
||||
slog.WarnContext(ctx, "Invalid assertion for client", slog.String("client", client.ID), slog.Any("error", err))
|
||||
return nil, &common.OidcClientAssertionInvalidError{}
|
||||
}
|
||||
return client, nil
|
||||
@@ -1695,3 +1833,19 @@ func (s *OidcService) getUserClaimsFromAuthorizedClient(ctx context.Context, aut
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) IsClientAccessibleToUser(ctx context.Context, clientID string, userID string) (bool, error) {
|
||||
var user model.User
|
||||
err := s.db.WithContext(ctx).Preload("UserGroups").First(&user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var client model.OidcClient
|
||||
err = s.db.WithContext(ctx).Preload("AllowedUserGroups").First(&client, "id = ?", clientID).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return s.IsUserGroupAllowedToAuthorize(user, client), nil
|
||||
}
|
||||
|
||||
@@ -171,8 +171,10 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
// Create the test clients
|
||||
// 1. Confidential client
|
||||
confidentialClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||
Name: "Confidential Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||
Name: "Confidential Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
},
|
||||
}, "test-user-id")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -182,20 +184,24 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
|
||||
// 2. Public client
|
||||
publicClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||
Name: "Public Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
IsPublic: true,
|
||||
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||
Name: "Public Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
IsPublic: true,
|
||||
},
|
||||
}, "test-user-id")
|
||||
require.NoError(t, err)
|
||||
|
||||
// 3. Confidential client with federated identity
|
||||
federatedClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||
Name: "Federated Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||
Name: "Federated Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
},
|
||||
}, "test-user-id")
|
||||
require.NoError(t, err)
|
||||
|
||||
federatedClient, err = s.UpdateClient(t.Context(), federatedClient.ID, dto.OidcClientCreateDto{
|
||||
federatedClient, err = s.UpdateClient(t.Context(), federatedClient.ID, dto.OidcClientUpdateDto{
|
||||
Name: federatedClient.Name,
|
||||
CallbackURLs: federatedClient.CallbackURLs,
|
||||
Credentials: dto.OidcClientCredentialsDto{
|
||||
|
||||
@@ -32,8 +32,7 @@ func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginati
|
||||
}
|
||||
|
||||
// As userCount is not a column we need to manually sort it
|
||||
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc"
|
||||
if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection {
|
||||
if sortedPaginationRequest.Sort.Column == "userCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
|
||||
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
|
||||
Group("user_groups.id").
|
||||
|
||||
@@ -3,16 +3,18 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
@@ -25,15 +27,23 @@ import (
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
jwtService *JwtService
|
||||
auditLogService *AuditLogService
|
||||
emailService *EmailService
|
||||
appConfigService *AppConfigService
|
||||
db *gorm.DB
|
||||
jwtService *JwtService
|
||||
auditLogService *AuditLogService
|
||||
emailService *EmailService
|
||||
appConfigService *AppConfigService
|
||||
customClaimService *CustomClaimService
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService) *UserService {
|
||||
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService, emailService: emailService, appConfigService: appConfigService}
|
||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService) *UserService {
|
||||
return &UserService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
auditLogService: auditLogService,
|
||||
emailService: emailService,
|
||||
appConfigService: appConfigService,
|
||||
customClaimService: customClaimService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
||||
@@ -45,7 +55,8 @@ func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPa
|
||||
|
||||
if searchTerm != "" {
|
||||
searchPattern := "%" + searchTerm + "%"
|
||||
query = query.Where("email LIKE ? OR first_name LIKE ? OR last_name LIKE ? OR username LIKE ?",
|
||||
query = query.Where(
|
||||
"email LIKE ? OR first_name LIKE ? OR last_name LIKE ? OR username LIKE ?",
|
||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
@@ -118,13 +129,14 @@ func (s *UserService) GetProfilePicture(ctx context.Context, userID string) (io.
|
||||
defaultPictureBytes := defaultPicture.Bytes()
|
||||
go func() {
|
||||
// Ensure the directory exists
|
||||
err = os.MkdirAll(defaultProfilePicturesDir, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create directory for default profile picture: %v", err)
|
||||
errInternal := os.MkdirAll(defaultProfilePicturesDir, os.ModePerm)
|
||||
if errInternal != nil {
|
||||
slog.Error("Failed to create directory for default profile picture", slog.Any("error", errInternal))
|
||||
return
|
||||
}
|
||||
if err := utils.SaveFileStream(bytes.NewReader(defaultPictureBytes), defaultPicturePath); err != nil {
|
||||
log.Printf("Failed to cache default profile picture for initials %s: %v", user.Initials(), err)
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -259,9 +271,53 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
|
||||
} else if err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
|
||||
// Apply default groups and claims for new non-LDAP users
|
||||
if !isLdapSync {
|
||||
if err := s.applySignupDefaults(ctx, &user, tx); err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) applySignupDefaults(ctx context.Context, user *model.User, tx *gorm.DB) error {
|
||||
config := s.appConfigService.GetDbConfig()
|
||||
|
||||
// Apply default user groups
|
||||
var groupIDs []string
|
||||
if v := config.SignupDefaultUserGroupIDs.Value; v != "" && v != "[]" {
|
||||
if err := json.Unmarshal([]byte(v), &groupIDs); err != nil {
|
||||
return fmt.Errorf("invalid SignupDefaultUserGroupIDs JSON: %w", err)
|
||||
}
|
||||
if len(groupIDs) > 0 {
|
||||
var groups []model.UserGroup
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", groupIDs).Find(&groups).Error; err != nil {
|
||||
return fmt.Errorf("failed to find default user groups: %w", err)
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(user).Association("UserGroups").Replace(groups); err != nil {
|
||||
return fmt.Errorf("failed to associate default user groups: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply default custom claims
|
||||
var claims []dto.CustomClaimCreateDto
|
||||
if v := config.SignupDefaultCustomClaims.Value; v != "" && v != "[]" {
|
||||
if err := json.Unmarshal([]byte(v), &claims); err != nil {
|
||||
return fmt.Errorf("invalid SignupDefaultCustomClaims JSON: %w", err)
|
||||
}
|
||||
if len(claims) > 0 {
|
||||
if _, err := s.customClaimService.updateCustomClaimsInternal(ctx, UserID, user.ID, claims, tx); err != nil {
|
||||
return fmt.Errorf("failed to apply default custom claims: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool) (model.User, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
@@ -339,13 +395,13 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, userID string, expiration time.Time) error {
|
||||
func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, userID string, ttl time.Duration) error {
|
||||
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsAdminEnabled.IsTrue()
|
||||
if isDisabled {
|
||||
return &common.OneTimeAccessDisabledError{}
|
||||
}
|
||||
|
||||
return s.requestOneTimeAccessEmailInternal(ctx, userID, "", expiration)
|
||||
return s.requestOneTimeAccessEmailInternal(ctx, userID, "", ttl)
|
||||
}
|
||||
|
||||
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error {
|
||||
@@ -365,11 +421,10 @@ func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context
|
||||
}
|
||||
}
|
||||
|
||||
expiration := time.Now().Add(15 * time.Minute)
|
||||
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, expiration)
|
||||
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute)
|
||||
}
|
||||
|
||||
func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, expiration time.Time) error {
|
||||
func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, ttl time.Duration) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -380,7 +435,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
||||
return err
|
||||
}
|
||||
|
||||
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, expiration, tx)
|
||||
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -393,7 +448,8 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
||||
// We use a background context here as this is running in a goroutine
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
innerCtx := context.Background()
|
||||
span := trace.SpanFromContext(ctx)
|
||||
innerCtx := trace.ContextWithSpan(context.Background(), span)
|
||||
|
||||
link := common.EnvConfig.AppURL + "/lc"
|
||||
linkWithCode := link + "/" + oneTimeAccessToken
|
||||
@@ -411,27 +467,29 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
||||
Code: oneTimeAccessToken,
|
||||
LoginLink: link,
|
||||
LoginLinkWithCode: linkWithCode,
|
||||
ExpirationString: utils.DurationToString(time.Until(expiration).Round(time.Second)),
|
||||
ExpirationString: utils.DurationToString(ttl),
|
||||
})
|
||||
if errInternal != nil {
|
||||
log.Printf("Failed to send email to '%s': %v\n", user.Email, errInternal)
|
||||
slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email))
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, expiresAt time.Time) (string, error) {
|
||||
return s.createOneTimeAccessTokenInternal(ctx, userID, expiresAt, s.db)
|
||||
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error) {
|
||||
return s.createOneTimeAccessTokenInternal(ctx, userID, ttl, s.db)
|
||||
}
|
||||
|
||||
func (s *UserService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, expiresAt time.Time, tx *gorm.DB) (string, error) {
|
||||
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, expiresAt)
|
||||
func (s *UserService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, ttl time.Duration, tx *gorm.DB) (string, error) {
|
||||
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, ttl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Create(oneTimeAccessToken).Error; err != nil {
|
||||
err = tx.WithContext(ctx).Create(oneTimeAccessToken).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -493,7 +551,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
|
||||
// Fetch the groups based on userGroupIds
|
||||
var groups []model.UserGroup
|
||||
if len(userGroupIds) > 0 {
|
||||
err = tx.
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Where("id IN (?)", userGroupIds).
|
||||
Find(&groups).
|
||||
@@ -631,17 +689,14 @@ func (s *UserService) disableUserInternal(ctx context.Context, userID string, tx
|
||||
Error
|
||||
}
|
||||
|
||||
func (s *UserService) CreateSignupToken(ctx context.Context, expiresAt time.Time, usageLimit int) (model.SignupToken, error) {
|
||||
return s.createSignupTokenInternal(ctx, expiresAt, usageLimit, s.db)
|
||||
}
|
||||
|
||||
func (s *UserService) createSignupTokenInternal(ctx context.Context, expiresAt time.Time, usageLimit int, tx *gorm.DB) (model.SignupToken, error) {
|
||||
signupToken, err := NewSignupToken(expiresAt, usageLimit)
|
||||
func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int) (model.SignupToken, error) {
|
||||
signupToken, err := NewSignupToken(ttl, usageLimit)
|
||||
if err != nil {
|
||||
return model.SignupToken{}, err
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Create(signupToken).Error; err != nil {
|
||||
err = s.db.WithContext(ctx).Create(signupToken).Error
|
||||
if err != nil {
|
||||
return model.SignupToken{}, err
|
||||
}
|
||||
|
||||
@@ -735,10 +790,10 @@ func (s *UserService) DeleteSignupToken(ctx context.Context, tokenID string) err
|
||||
return s.db.WithContext(ctx).Delete(&model.SignupToken{}, "id = ?", tokenID).Error
|
||||
}
|
||||
|
||||
func NewOneTimeAccessToken(userID string, expiresAt time.Time) (*model.OneTimeAccessToken, error) {
|
||||
func NewOneTimeAccessToken(userID string, ttl time.Duration) (*model.OneTimeAccessToken, error) {
|
||||
// If expires at is less than 15 minutes, use a 6-character token instead of 16
|
||||
tokenLength := 16
|
||||
if time.Until(expiresAt) <= 15*time.Minute {
|
||||
if ttl <= 15*time.Minute {
|
||||
tokenLength = 6
|
||||
}
|
||||
|
||||
@@ -747,25 +802,27 @@ func NewOneTimeAccessToken(userID string, expiresAt time.Time) (*model.OneTimeAc
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().Round(time.Second)
|
||||
o := &model.OneTimeAccessToken{
|
||||
UserID: userID,
|
||||
ExpiresAt: datatype.DateTime(expiresAt),
|
||||
ExpiresAt: datatype.DateTime(now.Add(ttl)),
|
||||
Token: randomString,
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func NewSignupToken(expiresAt time.Time, usageLimit int) (*model.SignupToken, error) {
|
||||
func NewSignupToken(ttl time.Duration, usageLimit int) (*model.SignupToken, error) {
|
||||
// Generate a random token
|
||||
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().Round(time.Second)
|
||||
token := &model.SignupToken{
|
||||
Token: randomString,
|
||||
ExpiresAt: datatype.DateTime(expiresAt),
|
||||
ExpiresAt: datatype.DateTime(now.Add(ttl)),
|
||||
UsageLimit: usageLimit,
|
||||
UsageCount: 0,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
@@ -24,8 +25,8 @@ type WebAuthnService struct {
|
||||
appConfigService *AppConfigService
|
||||
}
|
||||
|
||||
func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, appConfigService *AppConfigService) *WebAuthnService {
|
||||
webauthnConfig := &webauthn.Config{
|
||||
func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, appConfigService *AppConfigService) (*WebAuthnService, error) {
|
||||
wa, err := webauthn.New(&webauthn.Config{
|
||||
RPDisplayName: appConfigService.GetDbConfig().AppName.Value,
|
||||
RPID: utils.GetHostnameFromURL(common.EnvConfig.AppURL),
|
||||
RPOrigins: []string{common.EnvConfig.AppURL},
|
||||
@@ -44,15 +45,18 @@ func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *Au
|
||||
TimeoutUVD: time.Second * 60,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init webauthn object: %w", err)
|
||||
}
|
||||
wa, _ := webauthn.New(webauthnConfig)
|
||||
|
||||
return &WebAuthnService{
|
||||
db: db,
|
||||
webAuthn: wa,
|
||||
jwtService: jwtService,
|
||||
auditLogService: auditLogService,
|
||||
appConfigService: appConfigService,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string) (*model.PublicKeyCredentialCreationOptions, error) {
|
||||
@@ -70,8 +74,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
||||
Find(&user, "id = ?", userID).
|
||||
Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to load user: %w", err)
|
||||
}
|
||||
|
||||
options, session, err := s.webAuthn.BeginRegistration(
|
||||
@@ -80,7 +83,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
||||
webauthn.WithExclusions(user.WebAuthnCredentialDescriptors()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)
|
||||
}
|
||||
|
||||
sessionToStore := &model.WebauthnSession{
|
||||
@@ -94,12 +97,12 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
||||
Create(&sessionToStore).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to save WebAuthn session: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return &model.PublicKeyCredentialCreationOptions{
|
||||
@@ -115,13 +118,15 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
// Load & delete the session row
|
||||
var storedSession model.WebauthnSession
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
First(&storedSession, "id = ?", sessionID).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(&storedSession, "id = ?", sessionID).
|
||||
Error
|
||||
if err != nil {
|
||||
return model.WebauthnCredential{}, err
|
||||
return model.WebauthnCredential{}, fmt.Errorf("failed to load WebAuthn session: %w", err)
|
||||
}
|
||||
|
||||
session := webauthn.SessionData{
|
||||
@@ -136,12 +141,12 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
|
||||
Find(&user, "id = ?", userID).
|
||||
Error
|
||||
if err != nil {
|
||||
return model.WebauthnCredential{}, err
|
||||
return model.WebauthnCredential{}, fmt.Errorf("failed to load user: %w", err)
|
||||
}
|
||||
|
||||
credential, err := s.webAuthn.FinishRegistration(&user, session, r)
|
||||
if err != nil {
|
||||
return model.WebauthnCredential{}, err
|
||||
return model.WebauthnCredential{}, fmt.Errorf("failed to finish WebAuthn registration: %w", err)
|
||||
}
|
||||
|
||||
// Determine passkey name using AAGUID and User-Agent
|
||||
@@ -162,12 +167,12 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
|
||||
Create(&credentialToStore).
|
||||
Error
|
||||
if err != nil {
|
||||
return model.WebauthnCredential{}, err
|
||||
return model.WebauthnCredential{}, fmt.Errorf("failed to store WebAuthn credential: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return model.WebauthnCredential{}, err
|
||||
return model.WebauthnCredential{}, fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return credentialToStore, nil
|
||||
@@ -216,13 +221,15 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
// Load & delete the session row
|
||||
var storedSession model.WebauthnSession
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
First(&storedSession, "id = ?", sessionID).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(&storedSession, "id = ?", sessionID).
|
||||
Error
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
return model.User{}, "", fmt.Errorf("failed to load WebAuthn session: %w", err)
|
||||
}
|
||||
|
||||
session := webauthn.SessionData{
|
||||
@@ -329,3 +336,136 @@ func (s *WebAuthnService) UpdateCredential(ctx context.Context, userID, credenti
|
||||
func (s *WebAuthnService) updateWebAuthnConfig() {
|
||||
s.webAuthn.Config.RPDisplayName = s.appConfigService.GetDbConfig().AppName.Value
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(ctx context.Context, accessToken string) (string, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
token, err := s.jwtService.VerifyAccessToken(accessToken)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid access token: %w", err)
|
||||
}
|
||||
|
||||
userID, ok := token.Subject()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("access token does not contain user ID")
|
||||
}
|
||||
|
||||
// Check if token is issued less than a minute ago
|
||||
tokenExpiration, ok := token.IssuedAt()
|
||||
if !ok || time.Since(tokenExpiration) > time.Minute {
|
||||
return "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
First(&user, "id = ?", userID).
|
||||
Error
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load user: %w", err)
|
||||
}
|
||||
|
||||
reauthToken, err := s.createReauthenticationToken(ctx, tx, user.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return reauthToken, nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) CreateReauthenticationTokenWithWebauthn(ctx context.Context, sessionID string, credentialAssertionData *protocol.ParsedCredentialAssertionData) (string, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
// Retrieve and delete the session
|
||||
var storedSession model.WebauthnSession
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(&storedSession, "id = ? AND expires_at > ?", sessionID, datatype.DateTime(time.Now())).
|
||||
Error
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load WebAuthn session: %w", err)
|
||||
}
|
||||
|
||||
session := webauthn.SessionData{
|
||||
Challenge: storedSession.Challenge,
|
||||
Expires: storedSession.ExpiresAt.ToTime(),
|
||||
}
|
||||
|
||||
// Validate the credential assertion
|
||||
var user *model.User
|
||||
_, err = s.webAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (webauthn.User, error) {
|
||||
innerErr := tx.
|
||||
WithContext(ctx).
|
||||
Preload("Credentials").
|
||||
First(&user, "id = ?", string(userHandle)).
|
||||
Error
|
||||
if innerErr != nil {
|
||||
return nil, innerErr
|
||||
}
|
||||
return user, nil
|
||||
}, session, credentialAssertionData)
|
||||
|
||||
if err != nil || user == nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create reauthentication token
|
||||
token, err := s.createReauthenticationToken(ctx, tx, user.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) error {
|
||||
hashedToken := utils.CreateSha256Hash(token)
|
||||
result := tx.WithContext(ctx).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(&model.ReauthenticationToken{}, "token = ? AND user_id = ? AND expires_at > ?", hashedToken, userID, datatype.DateTime(time.Now()))
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) createReauthenticationToken(ctx context.Context, tx *gorm.DB, userID string) (string, error) {
|
||||
token, err := utils.GenerateRandomAlphanumericString(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reauthToken := model.ReauthenticationToken{
|
||||
Token: utils.CreateSha256Hash(token),
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(3 * time.Minute)),
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
err = tx.WithContext(ctx).Create(&reauthToken).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
@@ -57,12 +57,13 @@ func loadAAGUIDsFromFile() {
|
||||
// Read from embedded file system
|
||||
data, err := resources.FS.ReadFile("aaguids.json")
|
||||
if err != nil {
|
||||
log.Printf("Error reading embedded AAGUID file: %v", err)
|
||||
slog.Error("Error reading embedded AAGUID file", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aaguidMap); err != nil {
|
||||
log.Printf("Error unmarshalling AAGUID data: %v", err)
|
||||
err = json.Unmarshal(data, &aaguidMap)
|
||||
if err != nil {
|
||||
slog.Error("Error unmarshalling AAGUID data", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
@@ -136,3 +139,41 @@ func FileExists(path string) (bool, error) {
|
||||
}
|
||||
return !s.IsDir(), nil
|
||||
}
|
||||
|
||||
// IsWritableDir checks if a directory exists and is writable
|
||||
func IsWritableDir(dir string) (bool, error) {
|
||||
// Check if directory exists and it's actually a directory
|
||||
info, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("failed to stat '%s': %w", dir, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Generate a random suffix for the test file to avoid conflicts
|
||||
randomBytes := make([]byte, 8)
|
||||
_, err = io.ReadFull(rand.Reader, randomBytes)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
// Check if directory is writable by trying to create a temporary file
|
||||
testFile := filepath.Join(dir, ".pocketid_test_write_"+hex.EncodeToString(randomBytes))
|
||||
defer os.Remove(testFile)
|
||||
|
||||
file, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) || errors.Is(err, syscall.EROFS) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("failed to create test file: %w", err)
|
||||
}
|
||||
|
||||
_ = file.Close()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ func CreateProfilePicture(file io.Reader) (io.Reader, error) {
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err = imaging.Encode(pw, img, imaging.PNG)
|
||||
if err != nil {
|
||||
_ = pw.CloseWithError(fmt.Errorf("failed to encode image: %w", err))
|
||||
innerErr := imaging.Encode(pw, img, imaging.PNG)
|
||||
if innerErr != nil {
|
||||
_ = pw.CloseWithError(fmt.Errorf("failed to encode image: %w", innerErr))
|
||||
return
|
||||
}
|
||||
pw.Close()
|
||||
|
||||
42
backend/internal/utils/json_util.go
Normal file
42
backend/internal/utils/json_util.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSONDuration is a type that allows marshalling/unmarshalling a Duration
|
||||
type JSONDuration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
func (d JSONDuration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(d.String())
|
||||
}
|
||||
|
||||
func (d *JSONDuration) UnmarshalJSON(b []byte) error {
|
||||
var v any
|
||||
err := json.Unmarshal(b, &v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
// If the value is a number, interpret it as a number of seconds
|
||||
d.Duration = time.Duration(value) * time.Second
|
||||
return nil
|
||||
case string:
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
d.Duration, err = time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid duration")
|
||||
}
|
||||
}
|
||||
64
backend/internal/utils/json_util_test.go
Normal file
64
backend/internal/utils/json_util_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJSONDuration_MarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
duration time.Duration
|
||||
want string
|
||||
}{
|
||||
{time.Minute + 30*time.Second, "1m30s"},
|
||||
{0, "0s"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
d := JSONDuration{Duration: tc.duration}
|
||||
b, err := json.Marshal(d)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `"`+tc.want+`"`, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONDuration_UnmarshalJSON_String(t *testing.T) {
|
||||
var d JSONDuration
|
||||
err := json.Unmarshal([]byte(`"2h15m5s"`), &d)
|
||||
require.NoError(t, err)
|
||||
want := 2*time.Hour + 15*time.Minute + 5*time.Second
|
||||
assert.Equal(t, want, d.Duration)
|
||||
}
|
||||
|
||||
func TestJSONDuration_UnmarshalJSON_NumberSeconds(t *testing.T) {
|
||||
tests := []struct {
|
||||
json string
|
||||
want time.Duration
|
||||
}{
|
||||
{"0", 0},
|
||||
{"1", 1 * time.Second},
|
||||
{"2.25", 2 * time.Second}, // Milliseconds are truncated
|
||||
}
|
||||
for _, tc := range tests {
|
||||
var d JSONDuration
|
||||
err := json.Unmarshal([]byte(tc.json), &d)
|
||||
require.NoError(t, err, "input: %s", tc.json)
|
||||
assert.Equal(t, tc.want, d.Duration, "input: %s", tc.json)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONDuration_UnmarshalJSON_Invalid(t *testing.T) {
|
||||
cases := [][]byte{
|
||||
[]byte(`true`),
|
||||
[]byte(`{}`),
|
||||
[]byte(`"not-a-duration"`),
|
||||
}
|
||||
for _, b := range cases {
|
||||
var d JSONDuration
|
||||
err := json.Unmarshal(b, &d)
|
||||
require.Error(t, err, "input: %s", string(b))
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
@@ -47,26 +46,15 @@ func EncodeJWKBytes(key jwk.Key) ([]byte, error) {
|
||||
|
||||
// LoadKeyEncryptionKey loads the key encryption key for JWKs
|
||||
func LoadKeyEncryptionKey(envConfig *common.EnvConfigSchema, instanceID string) (kek []byte, err error) {
|
||||
// Try getting the key from the env var as string
|
||||
kekInput := []byte(envConfig.EncryptionKey)
|
||||
|
||||
// If there's nothing in the env, try loading from file
|
||||
if len(kekInput) == 0 && envConfig.EncryptionKeyFile != "" {
|
||||
kekInput, err = os.ReadFile(envConfig.EncryptionKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read key file '%s': %w", envConfig.EncryptionKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still no key, return
|
||||
if len(kekInput) == 0 {
|
||||
// If there's no key, return
|
||||
if len(envConfig.EncryptionKey) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We need a 256-bit key for encryption with AES-GCM-256
|
||||
// We use HMAC with SHA3-256 here to derive the key from the one passed as input
|
||||
// The key is tied to a specific instance of Pocket ID
|
||||
h := hmac.New(func() hash.Hash { return sha3.New256() }, kekInput)
|
||||
h := hmac.New(func() hash.Hash { return sha3.New256() }, []byte(envConfig.EncryptionKey))
|
||||
fmt.Fprint(h, "pocketid/"+instanceID+"/jwk-kek")
|
||||
kek = h.Sum(nil)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -35,9 +36,7 @@ func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gor
|
||||
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
|
||||
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
|
||||
|
||||
if sort.Direction == "" || (sort.Direction != "asc" && sort.Direction != "desc") {
|
||||
sort.Direction = "asc"
|
||||
}
|
||||
sort.Direction = NormalizeSortDirection(sort.Direction)
|
||||
|
||||
if sortFieldFound && isSortable {
|
||||
columnName := CamelCaseToSnakeCase(sort.Column)
|
||||
@@ -85,3 +84,16 @@ func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (Pagin
|
||||
ItemsPerPage: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NormalizeSortDirection(direction string) string {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
if d != "asc" && d != "desc" {
|
||||
return "asc"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func IsValidSortDirection(direction string) bool {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
return d == "asc" || d == "desc"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package signals
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -28,11 +28,11 @@ func SignalContext(parentCtx context.Context) context.Context {
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Println("Received interrupt signal. Shutting down…")
|
||||
slog.Info("Received interrupt signal. Shutting down…")
|
||||
cancel()
|
||||
|
||||
<-sigCh
|
||||
log.Println("Received a second interrupt signal. Forcing an immediate shutdown.")
|
||||
slog.Warn("Received a second interrupt signal. Forcing an immediate shutdown.")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
|
||||
85
backend/internal/utils/slogfanout.go
Normal file
85
backend/internal/utils/slogfanout.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// This file contains code adapted from https://github.com/samber/slog-multi
|
||||
// Source: https://github.com/samber/slog-multi/blob/ced84707f45ec9848138349ed58de178eedaa6f2/pipe.go
|
||||
// Copyright (C) 2023 Samuel Berthe
|
||||
// License: MIT (https://github.com/samber/slog-multi/blob/ced84707f45ec9848138349ed58de178eedaa6f2/LICENSE)
|
||||
|
||||
// LogFanoutHandler is a slog.Handler that sends logs to multiple destinations
|
||||
type LogFanoutHandler []slog.Handler
|
||||
|
||||
// Implements slog.Handler
|
||||
func (h LogFanoutHandler) Enabled(ctx context.Context, l slog.Level) bool {
|
||||
for i := range h {
|
||||
if h[i].Enabled(ctx, l) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Implements slog.Handler
|
||||
func (h LogFanoutHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
errs := make([]error, 0)
|
||||
for i := range h {
|
||||
if h[i].Enabled(ctx, r.Level) {
|
||||
err := try(func() error {
|
||||
return h[i].Handle(ctx, r.Clone())
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Implements slog.Handler
|
||||
func (h LogFanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
res := make(LogFanoutHandler, len(h))
|
||||
for i, v := range h {
|
||||
res[i] = v.WithAttrs(slices.Clone(attrs))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Implements slog.Handler
|
||||
func (h LogFanoutHandler) WithGroup(name string) slog.Handler {
|
||||
// https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
|
||||
res := make(LogFanoutHandler, len(h))
|
||||
for i, v := range h {
|
||||
res[i] = v.WithGroup(name)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func try(callback func() error) (err error) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
if e, ok := r.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("unexpected error: %+v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = callback()
|
||||
|
||||
return
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,25 +1,34 @@
|
||||
-- Normalize (form NFC) all existing values in the database
|
||||
UPDATE api_keys SET
|
||||
name = normalize(name, 'nfc'),
|
||||
description = normalize(description, 'nfc');
|
||||
DO $$
|
||||
BEGIN
|
||||
-- This function is available only if the server's encoding is UTF8
|
||||
IF current_setting('server_encoding') = 'UTF8' THEN
|
||||
UPDATE api_keys SET
|
||||
name = normalize(name, NFC),
|
||||
description = normalize(description, NFC);
|
||||
|
||||
UPDATE app_config_variables SET
|
||||
"value" = normalize("value", 'nfc')
|
||||
WHERE "key" = 'appName';
|
||||
UPDATE app_config_variables SET
|
||||
"value" = normalize("value", NFC)
|
||||
WHERE "key" = 'appName';
|
||||
|
||||
UPDATE custom_claims SET
|
||||
"key" = normalize("key", 'nfc'),
|
||||
"value" = normalize("value", 'nfc');
|
||||
UPDATE custom_claims SET
|
||||
"key" = normalize("key", NFC),
|
||||
"value" = normalize("value", NFC);
|
||||
|
||||
UPDATE oidc_clients SET
|
||||
name = normalize(name, 'nfc');
|
||||
UPDATE oidc_clients SET
|
||||
name = normalize(name, NFC);
|
||||
|
||||
UPDATE users SET
|
||||
username = normalize(username, 'nfc'),
|
||||
email = normalize(email, 'nfc'),
|
||||
first_name = normalize(first_name, 'nfc'),
|
||||
last_name = normalize(last_name, 'nfc');
|
||||
UPDATE users SET
|
||||
username = normalize(username, NFC),
|
||||
email = normalize(email, NFC),
|
||||
first_name = normalize(first_name, NFC),
|
||||
last_name = normalize(last_name, NFC);
|
||||
|
||||
UPDATE user_groups SET
|
||||
friendly_name = normalize(friendly_name, 'nfc'),
|
||||
"name" = normalize("name", 'nfc');
|
||||
UPDATE user_groups SET
|
||||
friendly_name = normalize(friendly_name, NFC),
|
||||
"name" = normalize("name", NFC);
|
||||
ELSE
|
||||
RAISE NOTICE 'Skipping normalization: server_encoding is %', current_setting('server_encoding');
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN launch_url;
|
||||
|
||||
ALTER TABLE user_authorized_oidc_clients DROP COLUMN last_used_at;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN launch_url TEXT;
|
||||
|
||||
ALTER TABLE user_authorized_oidc_clients ADD COLUMN last_used_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN requires_reauthentication;
|
||||
DROP TABLE IF EXISTS reauthentication_tokens;
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE TABLE reauthentication_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
user_id uuid NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE public.audit_logs
|
||||
DROP CONSTRAINT IF EXISTS audit_logs_user_id_fkey,
|
||||
ADD CONSTRAINT audit_logs_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES public.users (id);
|
||||
|
||||
ALTER TABLE public.oidc_authorization_codes
|
||||
DROP CONSTRAINT IF EXISTS oidc_authorization_codes_client_fk;
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE public.audit_logs
|
||||
DROP CONSTRAINT IF EXISTS audit_logs_user_id_fkey,
|
||||
ADD CONSTRAINT audit_logs_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES public.users (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.oidc_authorization_codes
|
||||
ADD CONSTRAINT oidc_authorization_codes_client_fk
|
||||
FOREIGN KEY (client_id) REFERENCES public.oidc_clients (id) ON DELETE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op because strings can't be converted to UUIDs
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Drop foreign keys that reference oidc_clients(id)
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
DROP CONSTRAINT IF EXISTS oidc_authorization_codes_client_fk;
|
||||
ALTER TABLE user_authorized_oidc_clients
|
||||
DROP CONSTRAINT IF EXISTS user_authorized_oidc_clients_client_id_fkey;
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
DROP CONSTRAINT IF EXISTS oidc_refresh_tokens_client_id_fkey;
|
||||
ALTER TABLE oidc_device_codes
|
||||
DROP CONSTRAINT IF EXISTS oidc_device_codes_client_id_fkey;
|
||||
ALTER TABLE oidc_clients_allowed_user_groups
|
||||
DROP CONSTRAINT IF EXISTS oidc_clients_allowed_user_groups_oidc_client_id_fkey;
|
||||
|
||||
-- Alter child columns to TEXT
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
ALTER COLUMN client_id TYPE TEXT USING client_id::text;
|
||||
|
||||
ALTER TABLE user_authorized_oidc_clients
|
||||
ALTER
|
||||
COLUMN client_id TYPE TEXT USING client_id::text;
|
||||
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ALTER
|
||||
COLUMN client_id TYPE TEXT USING client_id::text;
|
||||
|
||||
ALTER TABLE oidc_device_codes
|
||||
ALTER
|
||||
COLUMN client_id TYPE TEXT USING client_id::text;
|
||||
|
||||
ALTER TABLE oidc_clients_allowed_user_groups
|
||||
ALTER
|
||||
COLUMN oidc_client_id TYPE TEXT USING oidc_client_id::text;
|
||||
|
||||
-- Alter parent primary key column to TEXT
|
||||
ALTER TABLE oidc_clients
|
||||
ALTER
|
||||
COLUMN id TYPE TEXT USING id::text;
|
||||
|
||||
-- Recreate foreign keys with the new type
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
ADD CONSTRAINT oidc_authorization_codes_client_fk
|
||||
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE user_authorized_oidc_clients
|
||||
ADD CONSTRAINT user_authorized_oidc_clients_client_id_fkey
|
||||
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD CONSTRAINT oidc_refresh_tokens_client_id_fkey
|
||||
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE oidc_device_codes
|
||||
ADD CONSTRAINT oidc_device_codes_client_id_fkey
|
||||
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE oidc_clients_allowed_user_groups
|
||||
ADD CONSTRAINT oidc_clients_allowed_user_groups_oidc_client_id_fkey
|
||||
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN launch_url;
|
||||
|
||||
ALTER TABLE user_authorized_oidc_clients DROP COLUMN created_at;
|
||||
@@ -0,0 +1,16 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN launch_url TEXT;
|
||||
|
||||
CREATE TABLE user_authorized_oidc_clients_new
|
||||
(
|
||||
scope TEXT,
|
||||
user_id TEXT,
|
||||
client_id TEXT REFERENCES oidc_clients,
|
||||
last_used_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, client_id)
|
||||
);
|
||||
|
||||
INSERT INTO user_authorized_oidc_clients_new (scope, user_id, client_id, last_used_at)
|
||||
SELECT scope, user_id, client_id, unixepoch() FROM user_authorized_oidc_clients;
|
||||
|
||||
DROP TABLE user_authorized_oidc_clients;
|
||||
ALTER TABLE user_authorized_oidc_clients_new RENAME TO user_authorized_oidc_clients;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN requires_reauthentication;
|
||||
DROP INDEX IF EXISTS idx_reauthentication_tokens_token;
|
||||
DROP TABLE IF EXISTS reauthentication_tokens;
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE TABLE reauthentication_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op
|
||||
@@ -0,0 +1,177 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
---------------------------
|
||||
-- Delete all orphaned rows
|
||||
---------------------------
|
||||
UPDATE oidc_clients
|
||||
SET created_by_id = NULL
|
||||
WHERE created_by_id IS NOT NULL
|
||||
AND created_by_id NOT IN (SELECT id FROM users);
|
||||
|
||||
DELETE FROM oidc_authorization_codes WHERE user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM one_time_access_tokens WHERE user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM webauthn_credentials WHERE user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM audit_logs WHERE user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users);
|
||||
DELETE FROM api_keys WHERE user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users);
|
||||
|
||||
DELETE FROM oidc_refresh_tokens WHERE user_id NOT IN (SELECT id FROM users) OR client_id NOT IN (SELECT id FROM oidc_clients);
|
||||
DELETE FROM oidc_device_codes WHERE (user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users)) OR client_id NOT IN (SELECT id FROM oidc_clients);
|
||||
DELETE FROM user_authorized_oidc_clients WHERE user_id NOT IN (SELECT id FROM users) OR client_id NOT IN (SELECT id FROM oidc_clients);
|
||||
|
||||
DELETE FROM user_groups_users WHERE user_id NOT IN (SELECT id FROM users) OR user_group_id NOT IN (SELECT id FROM user_groups);
|
||||
|
||||
DELETE FROM custom_claims WHERE (user_id IS NOT NULL AND user_id NOT IN (SELECT id FROM users)) OR (user_group_id IS NOT NULL AND user_group_id NOT IN (SELECT id FROM user_groups));
|
||||
|
||||
DELETE FROM oidc_clients_allowed_user_groups WHERE oidc_client_id NOT IN (SELECT id FROM oidc_clients) OR user_group_id NOT IN (SELECT id FROM user_groups);
|
||||
|
||||
DELETE FROM reauthentication_tokens WHERE user_id NOT IN (SELECT id FROM users);
|
||||
|
||||
---------------------------
|
||||
-- Add missing foreign keys and edit cascade behavior where necessary
|
||||
---------------------------
|
||||
|
||||
-- reauthentication_tokens: add missing FK user_id → users
|
||||
CREATE TABLE reauthentication_tokens_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO reauthentication_tokens_new (id, created_at, token, expires_at, user_id)
|
||||
SELECT id, created_at, token, expires_at, user_id
|
||||
FROM reauthentication_tokens;
|
||||
DROP TABLE reauthentication_tokens;
|
||||
ALTER TABLE reauthentication_tokens_new RENAME TO reauthentication_tokens;
|
||||
CREATE INDEX idx_reauthentication_tokens_token
|
||||
ON reauthentication_tokens (token);
|
||||
|
||||
-- oidc_authorization_codes: add FK client_id, user_id → CASCADE
|
||||
CREATE TABLE oidc_authorization_codes_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
scope TEXT NOT NULL,
|
||||
nonce TEXT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
|
||||
code_challenge TEXT,
|
||||
code_challenge_method_sha256 NUMERIC
|
||||
);
|
||||
INSERT INTO oidc_authorization_codes_new
|
||||
(id, created_at, code, scope, nonce, expires_at, user_id, client_id, code_challenge, code_challenge_method_sha256)
|
||||
SELECT id, created_at, code, scope, nonce, expires_at, user_id, client_id, code_challenge, code_challenge_method_sha256
|
||||
FROM oidc_authorization_codes;
|
||||
DROP TABLE oidc_authorization_codes;
|
||||
ALTER TABLE oidc_authorization_codes_new RENAME TO oidc_authorization_codes;
|
||||
|
||||
-- user_authorized_oidc_clients: add FK user_id, cascade client_id
|
||||
CREATE TABLE user_authorized_oidc_clients_new
|
||||
(
|
||||
scope TEXT,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
|
||||
last_used_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, client_id)
|
||||
);
|
||||
INSERT INTO user_authorized_oidc_clients_new (scope, user_id, client_id, last_used_at)
|
||||
SELECT scope, user_id, client_id, last_used_at
|
||||
FROM user_authorized_oidc_clients;
|
||||
DROP TABLE user_authorized_oidc_clients;
|
||||
ALTER TABLE user_authorized_oidc_clients_new RENAME TO user_authorized_oidc_clients;
|
||||
|
||||
-- audit_logs: user_id → CASCADE
|
||||
CREATE TABLE audit_logs_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
country TEXT,
|
||||
city TEXT
|
||||
);
|
||||
INSERT INTO audit_logs_new
|
||||
(id, created_at, event, ip_address, user_agent, data, user_id, country, city)
|
||||
SELECT id, created_at, event, ip_address, user_agent, data, user_id, country, city
|
||||
FROM audit_logs;
|
||||
DROP TABLE audit_logs;
|
||||
ALTER TABLE audit_logs_new RENAME TO audit_logs;
|
||||
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
|
||||
CREATE INDEX idx_audit_logs_country ON audit_logs (country);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);
|
||||
CREATE INDEX idx_audit_logs_event ON audit_logs (event);
|
||||
CREATE INDEX idx_audit_logs_user_agent ON audit_logs (user_agent);
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
|
||||
|
||||
-- oidc_clients: created_by_id → SET NULL
|
||||
CREATE TABLE oidc_clients_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT,
|
||||
secret TEXT,
|
||||
callback_urls BLOB,
|
||||
image_type TEXT,
|
||||
created_by_id TEXT REFERENCES users ON DELETE SET NULL,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
pkce_enabled BOOLEAN DEFAULT FALSE,
|
||||
logout_callback_urls BLOB,
|
||||
credentials TEXT,
|
||||
launch_url TEXT,
|
||||
requires_reauthentication BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
INSERT INTO oidc_clients_new
|
||||
(id, created_at, name, secret, callback_urls, image_type, created_by_id,
|
||||
is_public, pkce_enabled, logout_callback_urls, credentials, launch_url, requires_reauthentication)
|
||||
SELECT id, created_at, name, secret, callback_urls, image_type, created_by_id,
|
||||
is_public, pkce_enabled, logout_callback_urls, credentials, launch_url, requires_reauthentication
|
||||
FROM oidc_clients;
|
||||
DROP TABLE oidc_clients;
|
||||
ALTER TABLE oidc_clients_new RENAME TO oidc_clients;
|
||||
|
||||
-- one_time_access_tokens: user_id → CASCADE
|
||||
CREATE TABLE one_time_access_tokens_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO one_time_access_tokens_new
|
||||
(id, created_at, token, expires_at, user_id)
|
||||
SELECT id, created_at, token, expires_at, user_id
|
||||
FROM one_time_access_tokens;
|
||||
DROP TABLE one_time_access_tokens;
|
||||
ALTER TABLE one_time_access_tokens_new RENAME TO one_time_access_tokens;
|
||||
|
||||
-- webauthn_credentials: user_id → CASCADE
|
||||
CREATE TABLE webauthn_credentials_new
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
attestation_type TEXT NOT NULL,
|
||||
transport BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users ON DELETE CASCADE,
|
||||
backup_eligible BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
backup_state BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
INSERT INTO webauthn_credentials_new
|
||||
(id, created_at, name, credential_id, public_key, attestation_type,
|
||||
transport, user_id, backup_eligible, backup_state)
|
||||
SELECT id, created_at, name, credential_id, public_key, attestation_type,
|
||||
transport, user_id, backup_eligible, backup_state
|
||||
FROM webauthn_credentials;
|
||||
DROP TABLE webauthn_credentials;
|
||||
ALTER TABLE webauthn_credentials_new RENAME TO webauthn_credentials;
|
||||
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA foreign_key_check;
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Kliknutím zkopírujete",
|
||||
"something_went_wrong": "Něco se pokazilo",
|
||||
"go_back_to_home": "Přejít zpět domů",
|
||||
"dont_have_access_to_your_passkey": "Nemáte přístup k Vašemu přístupovému klíči?",
|
||||
"alternative_sign_in_methods": "Alternativní způsoby přihlášení",
|
||||
"login_background": "Pozadí přihlašovací stránky",
|
||||
"logo": "Logo",
|
||||
"login_code": "Přihlašovací kód",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Veřejní klienti nemají client secret a místo toho používají PKCE. Povolte to, pokud je váš klient SPA nebo mobilní aplikace.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Exchange je bezpečnostní funkce, která zabraňuje útokům CSRF a narušení autorizačních kódů.",
|
||||
"requires_reauthentication": "Vyžaduje opětovné ověření",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Vyžaduje, aby se uživatelé při každém autorizačním pokusu znovu ověřili, i když jsou již přihlášeni.",
|
||||
"name_logo": "Logo {name}",
|
||||
"change_logo": "Změnit logo",
|
||||
"upload_logo": "Nahrát logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Kolikrát lze použít registrační token.",
|
||||
"expires": "Vyprší",
|
||||
"signup": "Zaregistrovat se",
|
||||
"user_creation": "Vytvoření uživatele",
|
||||
"configure_user_creation": "Spravujte nastavení vytváření uživatelů, včetně metod registrace a výchozích oprávnění pro nové uživatele.",
|
||||
"user_creation_groups_description": "Při registraci automaticky přiřaďte tyto skupiny novým uživatelům.",
|
||||
"user_creation_claims_description": "Při registraci automaticky přiřaďte tyto vlastní nároky novým uživatelům.",
|
||||
"user_creation_updated_successfully": "Nastavení pro vytváření uživatelů bylo úspěšně aktualizováno.",
|
||||
"signup_disabled_description": "Registrace uživatelů jsou kompletně zakázány. Nové uživatelské účty mohou vytvářet pouze správci.",
|
||||
"signup_requires_valid_token": "Pro vytvoření účtu je vyžadován platný registrační token",
|
||||
"validating_signup_token": "Ověřování registračního tokenu",
|
||||
"go_to_login": "Přejít na přihlášení",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Prozatím přeskočit",
|
||||
"account_created": "Účet vytvořen",
|
||||
"enable_user_signups": "Povolit registraci uživatelů",
|
||||
"enable_user_signups_description": "Určuje, zda by měla být funkce registrace uživatele povolena.",
|
||||
"enable_user_signups_description": "Rozhodněte, jak se uživatelé mohou registrovat pro nové účty v Pocket ID.",
|
||||
"user_signups_are_disabled": "Registrace uživatelů jsou v současné době zakázány",
|
||||
"create_signup_token": "Vytvořit registrační token",
|
||||
"view_active_signup_tokens": "Zobrazit aktivní registrační tokeny",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Načítání",
|
||||
"delete_signup_token": "Odstranit registrační token",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Jste si jisti, že chcete odstranit tento registrační token? Tuto akci nelze vrátit zpět.",
|
||||
"signup_disabled_description": "Registrace uživatelů jsou kompletně zakázány. Nové uživatelské účty mohou vytvářet pouze správci.",
|
||||
"signup_with_token": "Zaregistrovat se s tokenem",
|
||||
"signup_with_token_description": "Uživatelé se mohou zaregistrovat pouze pomocí platného registračního tokenu který byl vytvořen správcem.",
|
||||
"signup_open": "Otevřená registrace",
|
||||
"signup_open_description": "Kdokoli si může vytvořit nový účet bez omezení.",
|
||||
"of": "z",
|
||||
"skip_passkey_setup": "Přeskočit nastavení přístupového klíče",
|
||||
"skip_passkey_setup_description": "Je důrazně doporučeno nastavit přístupový klíč, bez něho se nebudete moci přihlásit, jakmile aktuální relace vyprší."
|
||||
"skip_passkey_setup_description": "Je důrazně doporučeno nastavit přístupový klíč, bez něho se nebudete moci přihlásit, jakmile aktuální relace vyprší.",
|
||||
"my_apps": "Moje aplikace",
|
||||
"no_apps_available": "Žádné aplikace nejsou k dispozici",
|
||||
"contact_your_administrator_for_app_access": "Kontaktujte svého správce, abyste získali přístup k aplikacím.",
|
||||
"launch": "Spuštění",
|
||||
"client_launch_url": "URL spuštění klienta",
|
||||
"client_launch_url_description": "URL adresa, která se otevře, když uživatel spustí aplikaci ze stránky Moje aplikace.",
|
||||
"client_name_description": "Název klienta, který se zobrazuje v uživatelském rozhraní Pocket ID.",
|
||||
"revoke_access": "Zrušit přístup",
|
||||
"revoke_access_description": "Zrušit přístup k <b>{clientName}</b>. <b>{clientName}</b> už nebude mít přístup k informacím o vašem účtu.",
|
||||
"revoke_access_successful": "Přístup k {clientName} byl úspěšně zrušen.",
|
||||
"last_signed_in_ago": "Naposledy přihlášen {time} před",
|
||||
"invalid_client_id": "ID klienta může obsahovat pouze písmena, číslice, podtržítka a pomlčky.",
|
||||
"custom_client_id_description": "Nastavte vlastní ID klienta, pokud to vyžaduje vaše aplikace. V opačném případě pole nechte prázdné, aby bylo vygenerováno náhodné ID.",
|
||||
"generated": "Vygenerováno"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Klik for at kopiere",
|
||||
"something_went_wrong": "Noget gik galt",
|
||||
"go_back_to_home": "Gå tilbage til hjem",
|
||||
"dont_have_access_to_your_passkey": "Har du ikke adgang til din adgangsnøgle?",
|
||||
"alternative_sign_in_methods": "Alternative login-metoder",
|
||||
"login_background": "Log ind baggrund",
|
||||
"logo": "Logo",
|
||||
"login_code": "Loginkode",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Public-klienter har ikke en klienthemmelighed. De er designet til mobil-, web- og native-apps, hvor hemmeligheder ikke kan opbevares sikkert.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange er en sikkerhedsfunktion, der beskytter mod CSRF- og authorization code-angreb.",
|
||||
"requires_reauthentication": "Kræver genbekræftelse",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Kræver, at brugerne skal godkende igen ved hver autorisation, selvom de allerede er logget ind.",
|
||||
"name_logo": "Logo for {name}",
|
||||
"change_logo": "Skift logo",
|
||||
"upload_logo": "Upload logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Antal gange, som tilmeldingstokenet kan bruges.",
|
||||
"expires": "Udløber",
|
||||
"signup": "Tilmeld",
|
||||
"user_creation": "Oprettelse af bruger",
|
||||
"configure_user_creation": "Administrer indstillinger for brugeroprettelse, herunder tilmeldingsmetoder og standardtilladelser for nye brugere.",
|
||||
"user_creation_groups_description": "Tildel disse grupper automatisk til nye brugere ved tilmelding.",
|
||||
"user_creation_claims_description": "Tildel disse brugerdefinerede krav automatisk til nye brugere ved tilmelding.",
|
||||
"user_creation_updated_successfully": "Indstillinger for brugeroprettelse opdateret.",
|
||||
"signup_disabled_description": "Brugerregistreringer er fuldstændigt deaktiveret. Kun administratorer kan oprette nye brugerkonti.",
|
||||
"signup_requires_valid_token": "Der kræves en gyldig tilmeldingstoken for at oprette en konto.",
|
||||
"validating_signup_token": "Validering af tilmeldingstoken",
|
||||
"go_to_login": "Gå til login",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Spring over for nu",
|
||||
"account_created": "Konto oprettet",
|
||||
"enable_user_signups": "Aktiver brugerregistrering",
|
||||
"enable_user_signups_description": "Om brugerregistreringsfunktionen skal være aktiveret.",
|
||||
"enable_user_signups_description": "Bestem, hvordan brugere kan oprette nye konti i Pocket ID.",
|
||||
"user_signups_are_disabled": "Brugerregistrering er i øjeblikket deaktiveret.",
|
||||
"create_signup_token": "Opret tilmeldingstoken",
|
||||
"view_active_signup_tokens": "Vis aktive tilmeldingstokener",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Indlæsning",
|
||||
"delete_signup_token": "Slet tilmeldingstoken",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Er du sikker på, at du vil slette denne tilmeldingstoken? Denne handling kan ikke fortrydes.",
|
||||
"signup_disabled_description": "Brugerregistreringer er fuldstændigt deaktiveret. Kun administratorer kan oprette nye brugerkonti.",
|
||||
"signup_with_token": "Tilmeld dig med token",
|
||||
"signup_with_token_description": "Brugere kan kun tilmelde sig ved hjælp af en gyldig tilmeldingstoken, der er oprettet af en administrator.",
|
||||
"signup_open": "Åben tilmelding",
|
||||
"signup_open_description": "Alle kan oprette en ny konto uden begrænsninger.",
|
||||
"of": "af",
|
||||
"skip_passkey_setup": "Spring Passkey-opsætning over",
|
||||
"skip_passkey_setup_description": "Det anbefales stærkt at oprette en adgangskode, da du ellers bliver låst ude af din konto, så snart sessionen udløber."
|
||||
"skip_passkey_setup_description": "Det anbefales stærkt at oprette en adgangskode, da du ellers bliver låst ude af din konto, så snart sessionen udløber.",
|
||||
"my_apps": "Mine apps",
|
||||
"no_apps_available": "Ingen apps tilgængelige",
|
||||
"contact_your_administrator_for_app_access": "Kontakt din administrator for at få adgang til applikationer.",
|
||||
"launch": "Start",
|
||||
"client_launch_url": "Kundens lancerings-URL",
|
||||
"client_launch_url_description": "Den URL, der åbnes, når en bruger starter appen fra siden Mine apps.",
|
||||
"client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.",
|
||||
"revoke_access": "Tilbagekald adgang",
|
||||
"revoke_access_description": "Tilbagekald adgang til <b>{clientName}</b>. <b>{clientName}</b> vil ikke længere kunne få adgang til dine kontooplysninger.",
|
||||
"revoke_access_successful": "Adgangen til {clientName} er blevet ophævet.",
|
||||
"last_signed_in_ago": "Sidst logget ind {time} siden",
|
||||
"invalid_client_id": "Kunde-ID må kun indeholde bogstaver, tal, understregninger og bindestreger.",
|
||||
"custom_client_id_description": "Indstil et brugerdefineret klient-id, hvis dette kræves af din applikation. Ellers skal du lade feltet være tomt for at generere et tilfældigt id.",
|
||||
"generated": "Genereret"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Zum Kopieren klicken",
|
||||
"something_went_wrong": "Etwas ist schiefgelaufen",
|
||||
"go_back_to_home": "Zurück zur Startseite",
|
||||
"dont_have_access_to_your_passkey": "Du hast keinen Zugriff auf deinen Passkey?",
|
||||
"alternative_sign_in_methods": "Andere Anmeldemöglichkeiten",
|
||||
"login_background": "Login Hintergrund",
|
||||
"logo": "Logo",
|
||||
"login_code": "Anmeldecode",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Öffentliche Clients haben kein Client-Geheimnis und verwenden stattdessen PKCE. Aktiviere dies, wenn dein Client eine SPA oder mobile App ist.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Der Public Key Code Exchange (öffentlicher Schlüsselaustausch) ist eine Sicherheitsfunktion, um CSRF Angriffe und Angriffe zum Abfangen von Autorisierungscodes zu verhindern.",
|
||||
"requires_reauthentication": "Erfordert erneute Authentifizierung",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Erfordert eine neue Authentifizierung bei jeder Autorisierung, auch wenn der Benutzer bereits angemeldet ist",
|
||||
"name_logo": "{name} Logo",
|
||||
"change_logo": "Logo ändern",
|
||||
"upload_logo": "Logo hochladen",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Wie oft der Anmeldetoken benutzt werden kann.",
|
||||
"expires": "Läuft ab",
|
||||
"signup": "Anmelden",
|
||||
"user_creation": "Benutzererstellung",
|
||||
"configure_user_creation": "Verwalte die Einstellungen für die Benutzererstellung, einschließlich der Anmeldemethoden und Standardberechtigungen für neue Benutzer.",
|
||||
"user_creation_groups_description": "Weise diese Gruppen neuen Benutzern bei der Anmeldung automatisch zu.",
|
||||
"user_creation_claims_description": "Weise diese benutzerdefinierten Ansprüche neuen Benutzern bei der Anmeldung automatisch zu.",
|
||||
"user_creation_updated_successfully": "Einstellungen für die Benutzererstellung erfolgreich aktualisiert.",
|
||||
"signup_disabled_description": "Benutzeranmeldungen sind komplett deaktiviert. Nur Admins können neue Benutzerkonten erstellen.",
|
||||
"signup_requires_valid_token": "Zum Erstellen eines Kontos brauchst du einen gültigen Anmeldetoken.",
|
||||
"validating_signup_token": "Anmeldungstoken bestätigen",
|
||||
"go_to_login": "Zum Login gehen",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Jetzt überspringen",
|
||||
"account_created": "Konto erstellt",
|
||||
"enable_user_signups": "Benutzeranmeldungen aktivieren",
|
||||
"enable_user_signups_description": "Ob die Funktion zur Benutzeranmeldung aktiviert werden soll.",
|
||||
"enable_user_signups_description": "Entscheide, wie sich Leute für neue Konten in Pocket ID anmelden können.",
|
||||
"user_signups_are_disabled": "Benutzeranmeldungen sind im Moment deaktiviert.",
|
||||
"create_signup_token": "Anmeldungstoken erstellen",
|
||||
"view_active_signup_tokens": "Aktive Anmeldetoken anzeigen",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Laden",
|
||||
"delete_signup_token": "Anmeldungstoken löschen",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Willst du diesen Anmeldetoken wirklich löschen? Das kannst du nicht rückgängig machen.",
|
||||
"signup_disabled_description": "Benutzeranmeldungen sind komplett deaktiviert. Nur Admins können neue Benutzerkonten erstellen.",
|
||||
"signup_with_token": "Mit Token anmelden",
|
||||
"signup_with_token_description": "Benutzer können sich nur mit einem gültigen Anmeldetoken anmelden, das von einem Administrator erstellt wurde.",
|
||||
"signup_open": "Anmeldung offen",
|
||||
"signup_open_description": "Jeder kann ohne Einschränkungen ein neues Konto erstellen.",
|
||||
"of": "von",
|
||||
"skip_passkey_setup": "Passwort-Einrichtung überspringen",
|
||||
"skip_passkey_setup_description": "Es wird dringend empfohlen, einen Passkey einzurichten, da du sonst nach Ablauf der Sitzung aus deinem Konto ausgesperrt wirst."
|
||||
"skip_passkey_setup_description": "Es wird dringend empfohlen, einen Passkey einzurichten, da du sonst nach Ablauf der Sitzung aus deinem Konto ausgesperrt wirst.",
|
||||
"my_apps": "Meine Apps",
|
||||
"no_apps_available": "Keine Apps verfügbar",
|
||||
"contact_your_administrator_for_app_access": "Frag deinen Administrator, wie du Zugriff auf die Anwendungen bekommst.",
|
||||
"launch": "Start",
|
||||
"client_launch_url": "Client-Start-URL",
|
||||
"client_launch_url_description": "Die URL, die geöffnet wird, wenn jemand die App von der Seite „Meine Apps“ startet.",
|
||||
"client_name_description": "Der Name des Clients, der in der Pocket ID-Benutzeroberfläche angezeigt wird.",
|
||||
"revoke_access": "Zugriff widerrufen",
|
||||
"revoke_access_description": "Zugriff widerrufen <b>{clientName}</b>. <b>{clientName}</b> kann nicht mehr auf deine Kontoinfos zugreifen.",
|
||||
"revoke_access_successful": "Der Zugriff auf „ {clientName} “ wurde erfolgreich gesperrt.",
|
||||
"last_signed_in_ago": "Zuletzt angemeldet vor {time} Stunden",
|
||||
"invalid_client_id": "Die Kunden-ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche haben.",
|
||||
"custom_client_id_description": "Gib eine eigene Client-ID ein, wenn deine App das braucht. Ansonsten lass das Feld leer, damit eine zufällige ID generiert wird.",
|
||||
"generated": "Generiert"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Click to copy",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"go_back_to_home": "Go back to home",
|
||||
"dont_have_access_to_your_passkey": "Don't have access to your passkey?",
|
||||
"alternative_sign_in_methods": "Alternative Sign In Methods",
|
||||
"login_background": "Login background",
|
||||
"logo": "Logo",
|
||||
"login_code": "Login Code",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"requires_reauthentication": "Requires Re-Authentication",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Change Logo",
|
||||
"upload_logo": "Upload Logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Number of times the signup token can be used.",
|
||||
"expires": "Expires",
|
||||
"signup": "Sign Up",
|
||||
"user_creation": "User Creation",
|
||||
"configure_user_creation": "Manage user creation settings, including signup methods and default permissions for new users.",
|
||||
"user_creation_groups_description": "Assign these groups automatically to new users upon signup.",
|
||||
"user_creation_claims_description": "Assign these custom claims automatically to new users upon signup.",
|
||||
"user_creation_updated_successfully": "User creation settings updated successfully.",
|
||||
"signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.",
|
||||
"signup_requires_valid_token": "A valid signup token is required to create an account",
|
||||
"validating_signup_token": "Validating signup token",
|
||||
"go_to_login": "Go to login",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Skip for now",
|
||||
"account_created": "Account Created",
|
||||
"enable_user_signups": "Enable User Signups",
|
||||
"enable_user_signups_description": "Whether the User Signup functionality should be enabled.",
|
||||
"enable_user_signups_description": "Decide how users can sign up for new accounts in Pocket ID.",
|
||||
"user_signups_are_disabled": "User signups are currently disabled",
|
||||
"create_signup_token": "Create Signup Token",
|
||||
"view_active_signup_tokens": "View Active Signup Tokens",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Loading",
|
||||
"delete_signup_token": "Delete Signup Token",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Are you sure you want to delete this signup token? This action cannot be undone.",
|
||||
"signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.",
|
||||
"signup_with_token": "Signup with token",
|
||||
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
|
||||
"signup_open": "Open Signup",
|
||||
"signup_open_description": "Anyone can create a new account without restrictions.",
|
||||
"of": "of",
|
||||
"skip_passkey_setup": "Skip Passkey Setup",
|
||||
"skip_passkey_setup_description": "It's highly recommended to set up a passkey because without one, you will be locked out of your account as soon as the session expires."
|
||||
"skip_passkey_setup_description": "It's highly recommended to set up a passkey because without one, you will be locked out of your account as soon as the session expires.",
|
||||
"my_apps": "My Apps",
|
||||
"no_apps_available": "No apps available",
|
||||
"contact_your_administrator_for_app_access": "Contact your administrator to get access to applications.",
|
||||
"launch": "Launch",
|
||||
"client_launch_url": "Client Launch URL",
|
||||
"client_launch_url_description": "The URL that will be opened when a user launches the app from the My Apps page.",
|
||||
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
|
||||
"revoke_access": "Revoke Access",
|
||||
"revoke_access_description": "Revoke access to <b>{clientName}</b>. <b>{clientName}</b> will no longer be able to access your account information.",
|
||||
"revoke_access_successful": "The access to {clientName} has been successfully revoked.",
|
||||
"last_signed_in_ago": "Last signed in {time} ago",
|
||||
"invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens",
|
||||
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||
"generated": "Generated"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Haz clic para copiar",
|
||||
"something_went_wrong": "Algo ha salido mal",
|
||||
"go_back_to_home": "Volver al Inicio",
|
||||
"dont_have_access_to_your_passkey": "¿No tiene acceso a su Passkey?",
|
||||
"alternative_sign_in_methods": "Métodos alternativos de inicio de sesión",
|
||||
"login_background": "Fondo de página de acceso",
|
||||
"logo": "Logotipo",
|
||||
"login_code": "Código de inicio de sesión",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "El intercambio de claves públicas es una función de seguridad que evita los ataques CSRF y la interceptación de códigos de autorización.",
|
||||
"requires_reauthentication": "Requiere reautenticación",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Requiere que los usuarios se autentiquen de nuevo en cada autorización, incluso si ya han iniciado sesión.",
|
||||
"name_logo": "{name} logotipo",
|
||||
"change_logo": "Cambiar logotipo",
|
||||
"upload_logo": "Subir Logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Número de veces que se puede utilizar el token de registro.",
|
||||
"expires": "Caduca",
|
||||
"signup": "Regístrate",
|
||||
"user_creation": "Registro",
|
||||
"configure_user_creation": "Gestiona la configuración de registro de usuarios, incluyendo los métodos de registro y los permisos por defecto para nuevos usuarios.",
|
||||
"user_creation_groups_description": "Asigna estos grupos automáticamente a los nuevos usuarios al registrarse.",
|
||||
"user_creation_claims_description": "Asigna estas reclamaciones personalizadas automáticamente a los nuevos usuarios al registrarse.",
|
||||
"user_creation_updated_successfully": "Configuración de registro actualizada correctamente.",
|
||||
"signup_disabled_description": "El registro de usuarios está completamente desactivado. Solo los administradores pueden crear nuevas cuentas de usuario.",
|
||||
"signup_requires_valid_token": "Se requiere un token de registro válido para crear una cuenta.",
|
||||
"validating_signup_token": "Validación del token de registro",
|
||||
"go_to_login": "Ir al inicio de sesión",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Cargando",
|
||||
"delete_signup_token": "Eliminar token de registro",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "¿Estás seguro de que deseas eliminar este token de registro? Esta acción no se puede deshacer.",
|
||||
"signup_disabled_description": "El registro de usuarios está completamente desactivado. Solo los administradores pueden crear nuevas cuentas de usuario.",
|
||||
"signup_with_token": "Regístrate con token",
|
||||
"signup_with_token_description": "Los usuarios solo pueden registrarse utilizando un token de registro válido creado por un administrador.",
|
||||
"signup_open": "Inscripción abierta",
|
||||
"signup_open_description": "Cualquiera puede crear una nueva cuenta sin restricciones.",
|
||||
"of": "de",
|
||||
"skip_passkey_setup": "Omitir la configuración de la clave de acceso",
|
||||
"skip_passkey_setup_description": "Es muy recomendable configurar una contraseña maestra, ya que sin ella no podrás acceder a tu cuenta una vez que expire la sesión."
|
||||
"skip_passkey_setup_description": "Es muy recomendable configurar una contraseña maestra, ya que sin ella no podrás acceder a tu cuenta una vez que expire la sesión.",
|
||||
"my_apps": "Mis aplicaciones",
|
||||
"no_apps_available": "No hay aplicaciones disponibles",
|
||||
"contact_your_administrator_for_app_access": "Ponte en contacto con tu administrador para obtener acceso a las aplicaciones.",
|
||||
"launch": "Lanzamiento",
|
||||
"client_launch_url": "URL de inicio del cliente",
|
||||
"client_launch_url_description": "La URL que se abrirá cuando un usuario inicie la aplicación desde la página Mis aplicaciones.",
|
||||
"client_name_description": "El nombre del cliente que aparece en la interfaz de usuario de Pocket ID.",
|
||||
"revoke_access": "Revocar acceso",
|
||||
"revoke_access_description": "Revocar el acceso a <b>{clientName}</b>. <b>{clientName}</b> ya no podrás acceder a la información de tu cuenta.",
|
||||
"revoke_access_successful": "El acceso a {clientName} ha sido revocado correctamente.",
|
||||
"last_signed_in_ago": "Último inicio de sesión en {time} hace",
|
||||
"invalid_client_id": "El ID de cliente solo puede contener letras, números, guiones bajos y guiones.",
|
||||
"custom_client_id_description": "Establece un ID de cliente personalizado si tu aplicación lo requiere. De lo contrario, déjalo en blanco para generar uno aleatorio.",
|
||||
"generated": "Generado"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Cliquer pour copier",
|
||||
"something_went_wrong": "Quelque chose n'a pas fonctionné",
|
||||
"go_back_to_home": "Retourner à l'accueil",
|
||||
"dont_have_access_to_your_passkey": "Vous n'avez pas accès à votre clé d'accès ?",
|
||||
"alternative_sign_in_methods": "Autres façons de se connecter",
|
||||
"login_background": "Arrière-plan de connexion",
|
||||
"logo": "Logo",
|
||||
"login_code": "Code de connexion",
|
||||
@@ -276,6 +276,8 @@
|
||||
"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",
|
||||
"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 l’interception de code d’autorisation.",
|
||||
"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.",
|
||||
"name_logo": "Logo {name}",
|
||||
"change_logo": "Changer le logo",
|
||||
"upload_logo": "Télécharger un logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Nombre de fois que le jeton d'inscription peut être utilisé.",
|
||||
"expires": "Expire",
|
||||
"signup": "S'inscrire",
|
||||
"user_creation": "Création d'un utilisateur",
|
||||
"configure_user_creation": "Gère les paramètres de création des utilisateurs, comme les méthodes d'inscription et les autorisations par défaut pour les nouveaux utilisateurs.",
|
||||
"user_creation_groups_description": "Attribuez automatiquement ces groupes aux nouveaux utilisateurs lors de leur inscription.",
|
||||
"user_creation_claims_description": "Attribuez automatiquement ces revendications personnalisées aux nouveaux utilisateurs lors de leur inscription.",
|
||||
"user_creation_updated_successfully": "Les paramètres de création d'utilisateur ont été mis à jour.",
|
||||
"signup_disabled_description": "Les inscriptions utilisateur sont complètement désactivées. Seuls les administrateurs peuvent créer de nouveaux comptes utilisateur.",
|
||||
"signup_requires_valid_token": "Un jeton d'inscription valide est requis pour créer un compte.",
|
||||
"validating_signup_token": "Validation du jeton d'inscription",
|
||||
"go_to_login": "Aller à la connexion",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Ignorer pour le moment",
|
||||
"account_created": "Compte créé",
|
||||
"enable_user_signups": "Activer les inscriptions utilisateur",
|
||||
"enable_user_signups_description": "Détermine si la fonctionnalité d'inscription des utilisateurs doit être activée.",
|
||||
"enable_user_signups_description": "Décide comment les utilisateurs peuvent créer de nouveaux comptes dans Pocket ID.",
|
||||
"user_signups_are_disabled": "Les inscriptions utilisateur sont actuellement désactivées",
|
||||
"create_signup_token": "Créer un jeton d'inscription",
|
||||
"view_active_signup_tokens": "Voir les jetons d'inscription actifs",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Chargement",
|
||||
"delete_signup_token": "Supprimer le jeton d'inscription",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Êtes-vous sûr de vouloir supprimer ce jeton d'inscription ? Cette action est irréversible.",
|
||||
"signup_disabled_description": "Les inscriptions utilisateur sont complètement désactivées. Seuls les administrateurs peuvent créer de nouveaux comptes utilisateur.",
|
||||
"signup_with_token": "Inscription avec jeton",
|
||||
"signup_with_token_description": "Les utilisateurs ne peuvent s'inscrire qu'en utilisant un jeton d'inscription valide créé par un administrateur.",
|
||||
"signup_open": "Inscription ouverte",
|
||||
"signup_open_description": "Toute personne peut créer un nouveau compte sans restriction.",
|
||||
"of": "sur",
|
||||
"skip_passkey_setup": "Ignorer la configuration de la 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",
|
||||
"no_apps_available": "Aucune appli disponible",
|
||||
"contact_your_administrator_for_app_access": "Contacte ton administrateur pour avoir accès aux applications.",
|
||||
"launch": "Lancement",
|
||||
"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_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
|
||||
"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_successful": "L'accès à {clientName} a été supprimé.",
|
||||
"last_signed_in_ago": "Dernière connexion il y a {time} il y a",
|
||||
"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é.",
|
||||
"generated": "Généré"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Clicca per copiare",
|
||||
"something_went_wrong": "Qualcosa è andato storto",
|
||||
"go_back_to_home": "Torna alla home",
|
||||
"dont_have_access_to_your_passkey": "Non hai accesso alla tua passkey?",
|
||||
"alternative_sign_in_methods": "Metodi di accesso alternativi",
|
||||
"login_background": "Sfondo di accesso",
|
||||
"logo": "Logo",
|
||||
"login_code": "Codice di accesso",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "I client pubblici non hanno un client secret e utilizzano PKCE. Abilita questa opzione se il tuo client è una SPA o un'app mobile.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Il Public Key Code Exchange è una funzionalità di sicurezza per prevenire attacchi CSRF e intercettazione del codice di autorizzazione.",
|
||||
"requires_reauthentication": "È necessario effettuare nuovamente l'autenticazione",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Chiede agli utenti di fare di nuovo l'autenticazione ogni volta che autorizzano qualcosa, anche se hanno già fatto l'accesso.",
|
||||
"name_logo": "Logo di {name}",
|
||||
"change_logo": "Cambia Logo",
|
||||
"upload_logo": "Carica Logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Numero di volte che il codice d'iscrizione può essere usato.",
|
||||
"expires": "Scadenza",
|
||||
"signup": "Registrati",
|
||||
"user_creation": "Creazione utente",
|
||||
"configure_user_creation": "Gestisci le impostazioni per creare nuovi utenti, come i metodi di registrazione e i permessi di default per quelli nuovi.",
|
||||
"user_creation_groups_description": "Assegna automaticamente questi gruppi ai nuovi utenti al momento della registrazione.",
|
||||
"user_creation_claims_description": "Assegna automaticamente queste richieste personalizzate ai nuovi utenti al momento della registrazione.",
|
||||
"user_creation_updated_successfully": "Le impostazioni per creare un utente sono state aggiornate.",
|
||||
"signup_disabled_description": "Le iscrizioni utente sono completamente disabilitate. Solo gli amministratori possono creare nuovi account utente.",
|
||||
"signup_requires_valid_token": "È necessario un codice d'iscrizione valido per creare un account",
|
||||
"validating_signup_token": "Convalida codice d'iscrizione",
|
||||
"go_to_login": "Vai alla login",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Salta per ora",
|
||||
"account_created": "Account creato",
|
||||
"enable_user_signups": "Abilita Iscrizioni Utente",
|
||||
"enable_user_signups_description": "Indica se la funzionalità di registrazione utente deve essere abilitata.",
|
||||
"enable_user_signups_description": "Decidi come gli utenti possono registrarsi per creare nuovi account in Pocket ID.",
|
||||
"user_signups_are_disabled": "Le iscrizioni utente sono attualmente disattivate",
|
||||
"create_signup_token": "Crea Codice d'iscrizione",
|
||||
"view_active_signup_tokens": "Visualizza codici d'iscrizione attivi",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Caricamento",
|
||||
"delete_signup_token": "Elimina Codice d'iscrizione",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Sei sicuro di voler eliminare questo codice d'iscrizione? Questa azione non può essere annullata.",
|
||||
"signup_disabled_description": "Le iscrizioni utente sono completamente disabilitate. Solo gli amministratori possono creare nuovi account utente.",
|
||||
"signup_with_token": "Registrati con codice",
|
||||
"signup_with_token_description": "Gli utenti possono registrarsi solo usando un codice d'iscrizione valido, creato da un amministratore.",
|
||||
"signup_open": "Apri Registrazione",
|
||||
"signup_open_description": "Chiunque può creare un nuovo account senza restrizioni.",
|
||||
"of": "di",
|
||||
"skip_passkey_setup": "Salta Impostazione Passkey",
|
||||
"skip_passkey_setup_description": "Si consiglia vivamente di impostare una passkey perché senza di essa, sarai tagliato fuori dal tuo account non appena scadrà la sessione."
|
||||
"skip_passkey_setup_description": "Si consiglia vivamente di impostare una passkey perché senza di essa, sarai tagliato fuori dal tuo account non appena scadrà la sessione.",
|
||||
"my_apps": "Le mie app",
|
||||
"no_apps_available": "Non ci sono app disponibili",
|
||||
"contact_your_administrator_for_app_access": "Contatta il tuo amministratore per accedere alle app.",
|
||||
"launch": "Lancio",
|
||||
"client_launch_url": "URL di avvio del cliente",
|
||||
"client_launch_url_description": "L'URL che si aprirà quando qualcuno avvia l'app dalla pagina Le mie app.",
|
||||
"client_name_description": "Il nome del cliente che appare nell'interfaccia utente Pocket ID.",
|
||||
"revoke_access": "Revoca accesso",
|
||||
"revoke_access_description": "Revoca l'accesso a <b>{clientName}</b>. <b>{clientName}</b> non potrà più accedere alle informazioni del tuo account.",
|
||||
"revoke_access_successful": "L'accesso a {clientName} è stato revocato con successo.",
|
||||
"last_signed_in_ago": "Ultimo accesso {time} fa",
|
||||
"invalid_client_id": "L'ID cliente può contenere solo lettere, numeri, trattini bassi e trattini.",
|
||||
"custom_client_id_description": "Imposta un ID cliente personalizzato se la tua app lo richiede. Altrimenti, lascia vuoto per generarne uno casuale.",
|
||||
"generated": "Generato"
|
||||
}
|
||||
|
||||
444
frontend/messages/ko.json
Normal file
444
frontend/messages/ko.json
Normal file
@@ -0,0 +1,444 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "내 계정",
|
||||
"logout": "로그아웃",
|
||||
"confirm": "확인",
|
||||
"docs": "문서",
|
||||
"key": "키",
|
||||
"value": "값",
|
||||
"remove_custom_claim": "사용자 정의 클레임 제거",
|
||||
"add_custom_claim": "사용자 정의 클레임 추가",
|
||||
"add_another": "추가",
|
||||
"select_a_date": "날짜 선택",
|
||||
"select_file": "파일 선택",
|
||||
"profile_picture": "프로필 사진",
|
||||
"profile_picture_is_managed_by_ldap_server": "프로필 사진이 LDAP 서버에서 관리되어 여기에서 변경할 수 없습니다.",
|
||||
"click_profile_picture_to_upload_custom": "프로필 사진을 클릭하여 파일에서 사용자 정의 사진을 업로드하세요.",
|
||||
"image_should_be_in_format": "이미지는 PNG 또는 JPEG 형식이여야 합니다.",
|
||||
"items_per_page": "페이지당 항목",
|
||||
"no_items_found": "항목 없음",
|
||||
"search": "검색...",
|
||||
"expand_card": "카드 확장",
|
||||
"copied": "복사됨",
|
||||
"click_to_copy": "클릭하여 복사",
|
||||
"something_went_wrong": "문제가 발생했습니다",
|
||||
"go_back_to_home": "홈으로 돌아가기",
|
||||
"alternative_sign_in_methods": "대체 로그인 방법",
|
||||
"login_background": "로그인 배경",
|
||||
"logo": "로고",
|
||||
"login_code": "로그인 코드",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "패스키 없이 한 번 로그인 할 수 있는 로그인 코드를 생성합니다.",
|
||||
"one_hour": "1시간",
|
||||
"twelve_hours": "12시간",
|
||||
"one_day": "1일",
|
||||
"one_week": "1주",
|
||||
"one_month": "1달",
|
||||
"expiration": "만료",
|
||||
"generate_code": "코드 생성",
|
||||
"name": "이름",
|
||||
"browser_unsupported": "지원되지 않는 브라우저",
|
||||
"this_browser_does_not_support_passkeys": "이 브라우저는 패스키를 지원하지 않습니다. 다른 로그인 방법을 사용하세요.",
|
||||
"an_unknown_error_occurred": "알 수 없는 오류 발생",
|
||||
"authentication_process_was_aborted": "인증 프로세스가 중단되었습니다",
|
||||
"error_occurred_with_authenticator": "인증기에서 오류가 발생했습니다",
|
||||
"authenticator_does_not_support_discoverable_credentials": "인증기가 발견 가능한 자격 증명을 지원하지 않습니다",
|
||||
"authenticator_does_not_support_resident_keys": "인증기가 레지던트 키를 지원하지 않습니다",
|
||||
"passkey_was_previously_registered": "이 패스키는 이미 등록되었습니다",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "인증기가 요청된 알고리즘 중 어느 것도 지원하지 않습니다",
|
||||
"authenticator_timed_out": "인증기가 시간 초과되었습니다",
|
||||
"critical_error_occurred_contact_administrator": "치명적인 오류가 발생했습니다. 관리자에게 연락해주세요.",
|
||||
"sign_in_to": "{name}에 로그인",
|
||||
"client_not_found": "클라이언트를 찾을 수 없습니다",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b>이(가) 다음 정보에 접근하려고 합니다:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "{appName} 계정으로 <b>{client}</b>에 로그인하시겠습니까?",
|
||||
"email": "이메일",
|
||||
"view_your_email_address": "이메일 주소 확인",
|
||||
"profile": "프로필",
|
||||
"view_your_profile_information": "프로필 정보 확인",
|
||||
"groups": "그룹",
|
||||
"view_the_groups_you_are_a_member_of": "멤버인 그룹 정보 확인",
|
||||
"cancel": "취소",
|
||||
"sign_in": "로그인",
|
||||
"try_again": "다시 시도",
|
||||
"client_logo": "클라이언트 로고",
|
||||
"sign_out": "로그아웃",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "{appName}에서 계정 <b>{username}</b>을 로그아웃하시겠습니까?",
|
||||
"sign_in_to_appname": "{appName}에 로그인",
|
||||
"please_try_to_sign_in_again": "다시 로그인해주세요.",
|
||||
"authenticate_with_passkey_to_access_account": "패스키로 본인 인증하여 계정에 접근하세요.",
|
||||
"authenticate": "인증",
|
||||
"please_try_again": "다시 시도해주세요.",
|
||||
"continue": "계속",
|
||||
"alternative_sign_in": "다른 로그인 방법",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "패스키에 접근할 수 없는 경우 다음 방법 중 하나를 이용하여 로그인할 수 있습니다.",
|
||||
"use_your_passkey_instead": "대신 패스키 이용하기",
|
||||
"email_login": "이메일 로그인",
|
||||
"enter_a_login_code_to_sign_in": "로그인 코드를 입력하여 로그인하세요.",
|
||||
"request_a_login_code_via_email": "이메일로 로그인 코드를 요청합니다.",
|
||||
"go_back": "뒤로 가기",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "입력한 이메일 주소가 시스템에 존재하는 경우 이메일이 발송됩니다.",
|
||||
"enter_code": "코드 입력",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "이메일 주소를 입력하여 로그인 코드가 포함된 이메일을 받을 수 있습니다.",
|
||||
"your_email": "이메일 주소",
|
||||
"submit": "확인",
|
||||
"enter_the_code_you_received_to_sign_in": "로그인하기 위해 받은 코드를 입력하세요.",
|
||||
"code": "코드",
|
||||
"invalid_redirect_url": "잘못된 리다이렉트 URL",
|
||||
"audit_log": "감사 로그",
|
||||
"users": "사용자",
|
||||
"user_groups": "사용자 그룹",
|
||||
"oidc_clients": "OIDC 클라이언트",
|
||||
"api_keys": "API 키",
|
||||
"application_configuration": "애플리케이션 설정",
|
||||
"settings": "설정",
|
||||
"update_pocket_id": "Pocket ID 업데이트",
|
||||
"powered_by": "제공:",
|
||||
"see_your_account_activities_from_the_last_3_months": "지난 3개월 동안의 계정 활동을 확인하세요.",
|
||||
"time": "시간",
|
||||
"event": "이벤트",
|
||||
"approximate_location": "대략적인 위치",
|
||||
"ip_address": "IP 주소",
|
||||
"device": "기기",
|
||||
"client": "클라이언트",
|
||||
"unknown": "알 수 없음",
|
||||
"account_details_updated_successfully": "계정 세부 사항이 성공적으로 업데이트되었습니다",
|
||||
"profile_picture_updated_successfully": "프로필 사진이 성공적으로 업데이트되었습니다. 업데이트 적용까지 몇 분 정도 걸릴 수 있습니다.",
|
||||
"account_settings": "계정 설정",
|
||||
"passkey_missing": "패스키가 없습니다",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "계정 접근 권한을 잃지 않기 위해 패스키를 추가해주세요.",
|
||||
"single_passkey_configured": "패스키가 하나만 구성되었습니다",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "계정 접근 권한을 잃지 않기 위해 패스키를 두 개 이상 추가하는 것이 권장됩니다.",
|
||||
"account_details": "계정 세부 사항",
|
||||
"passkeys": "패스키",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "사용자 인증에 사용하는 패스키를 관리하세요.",
|
||||
"add_passkey": "패스키 추가",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "패스키 없이 다른 기기에서 로그인하기 위한 일회용 로그인 코드를 생성합니다.",
|
||||
"create": "생성",
|
||||
"first_name": "이름",
|
||||
"last_name": "성",
|
||||
"username": "사용자 이름",
|
||||
"save": "저장",
|
||||
"username_can_only_contain": "사용자 이름은 영어 소문자, 숫자, 밑줄, 점, 하이픈, '@' 기호만 포함할 수 있습니다",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "다음 코드를 사용하여 로그인하세요. 이 코드는 15분 후에 만료됩니다.",
|
||||
"or_visit": "또는",
|
||||
"added_on": "추가:",
|
||||
"rename": "이름 변경",
|
||||
"delete": "삭제",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "이 패스키를 삭제하시겠습니까?",
|
||||
"passkey_deleted_successfully": "패스키가 성공적으로 삭제되었습니다",
|
||||
"delete_passkey_name": "{passkeyName} 삭제",
|
||||
"passkey_name_updated_successfully": "패스키 이름이 성공적으로 업데이트되었습니다",
|
||||
"name_passkey": "패스키 이름",
|
||||
"name_your_passkey_to_easily_identify_it_later": "패스키의 이름을 지정하여 나중에 쉽게 구분할 수 있도록 합니다.",
|
||||
"create_api_key": "API 키 생성",
|
||||
"add_a_new_api_key_for_programmatic_access": "프로그램 접근을 위해 새로운 API 키를 추가합니다.",
|
||||
"add_api_key": "API 키 추가",
|
||||
"manage_api_keys": "API 키 관리",
|
||||
"api_key_created": "API 키 생성됨",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "보안상의 이유로 이 키는 한 번만 표시됩니다. 안전하게 보관해 주세요.",
|
||||
"description": "설명",
|
||||
"api_key": "API 키",
|
||||
"close": "닫기",
|
||||
"name_to_identify_this_api_key": "API 키를 구분하기 위한 이름.",
|
||||
"expires_at": "만료일",
|
||||
"when_this_api_key_will_expire": "API 키의 만료일.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "이 키의 목적을 알기 위한 설명. (선택)",
|
||||
"expiration_date_must_be_in_the_future": "만료일은 미래의 날짜여야 합니다",
|
||||
"revoke_api_key": "API 키 취소",
|
||||
"never": "없음",
|
||||
"revoke": "취소",
|
||||
"api_key_revoked_successfully": "API 키가 성공적으로 취소되었습니다",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "API 키 \"{apiKeyName}\"를 정말로 취소하시겠습니까? 이 키를 사용하는 모든 통합이 작동하지 않습니다.",
|
||||
"last_used": "마지막 사용",
|
||||
"actions": "동작",
|
||||
"images_updated_successfully": "이미지가 성공적으로 업데이트되었습니다",
|
||||
"general": "일반",
|
||||
"configure_smtp_to_send_emails": "새로운 기기나 위치에서 로그인 감지 시 이메일 알림을 활성화합니다.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP 서버에서 사용자와 그룹을 동기화하기 위해 LDAP을 구성합니다.",
|
||||
"images": "이미지",
|
||||
"update": "업데이트",
|
||||
"email_configuration_updated_successfully": "이메일 설정이 성공적으로 업데이트되었습니다",
|
||||
"save_changes_question": "변경 내용을 저장하시겠습니까?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "테스트 이메일을 전송하기 전에 변경 내용을 저장해야 합니다. 지금 저장하시겠습니까?",
|
||||
"save_and_send": "저장하고 전송하기",
|
||||
"test_email_sent_successfully": "테스트 이메일이 성공적으로 귀하의 이메일 주소에 전송되었습니다.",
|
||||
"failed_to_send_test_email": "테스트 이메일 전송에 실패했씁니다. 자세한 내용을 서버 로그를 확인하세요.",
|
||||
"smtp_configuration": "SMTP 구성",
|
||||
"smtp_host": "SMTP 호스트",
|
||||
"smtp_port": "SMTP 포트",
|
||||
"smtp_user": "SMTP 사용자",
|
||||
"smtp_password": "SMTP 비밀번호",
|
||||
"smtp_from": "SMTP 발신자",
|
||||
"smtp_tls_option": "SMTP TLS 옵션",
|
||||
"email_tls_option": "이메일 TLS 옵션",
|
||||
"skip_certificate_verification": "인증서 검증 건너뛰기",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "이 옵션은 자체 설명 인증서에 유용할 수 있습니다.",
|
||||
"enabled_emails": "이메일 활성화",
|
||||
"email_login_notification": "이메일 로그인 알림",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "사용자가 새로운 기기에서 로그인할 때 이메일을 전송합니다.",
|
||||
"emai_login_code_requested_by_user": "사용자가 요청한 이메일 로그인 코드",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "사용자가 이메일로 전송된 로그인 코드를 요청하여 패스키를 우회할 수 있도록 합니다. 이 기능은 사용자의 이메일 접근 권한이 있는 누구나 접근할 수 있어 보안이 크게 약화됩니다.",
|
||||
"email_login_code_from_admin": "관리자 로그인 코드 전송",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "관리자가 사용자에게 로그인 코드를 전송할 수 있게 합니다.",
|
||||
"send_test_email": "테스트 이메일 보내기",
|
||||
"application_configuration_updated_successfully": "애플리케이션 구성이 성공적으로 업데이트되었습니다",
|
||||
"application_name": "애플리케이션 이름",
|
||||
"session_duration": "세션 기간",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "사용자가 다시 로그인하기 전 세션의 시간(분).",
|
||||
"enable_self_account_editing": "셀프 계정 편집 활성화",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "사용자가 자신의 계정 정보를 편집할 수 있습니다.",
|
||||
"emails_verified": "이메일 인증됨",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "OIDC 클라이언트에게 사용자의 이메일이 인증된 것으로 표시합니다.",
|
||||
"ldap_configuration_updated_successfully": "LDAP 구성이 성공적으로 업데이트되었습니다",
|
||||
"ldap_disabled_successfully": "LDAP가 성공적으로 비활성화되었습니다",
|
||||
"ldap_sync_finished": "LDAP 동기화 완료",
|
||||
"client_configuration": "클라이언트 구성",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP 바인드 DN",
|
||||
"ldap_bind_password": "LDAP 바인드 비밀번호",
|
||||
"ldap_base_dn": "LDAP 베이스 DN",
|
||||
"user_search_filter": "사용자 검색 필터",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "사용자 검색 및 동기화를 위한 검색 필터.",
|
||||
"groups_search_filter": "그룹 검색 필터",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "그룹 검색 및 동기화를 위한 검색 필터.",
|
||||
"attribute_mapping": "속성 매핑",
|
||||
"user_unique_identifier_attribute": "사용자 고유 식별자 속성",
|
||||
"the_value_of_this_attribute_should_never_change": "이 속성의 값은 절대 변경되면 안 됩니다.",
|
||||
"username_attribute": "사용자 이름 속성",
|
||||
"user_mail_attribute": "사용자 이메일 속성",
|
||||
"user_first_name_attribute": "이름 속성",
|
||||
"user_last_name_attribute": "성 속성",
|
||||
"user_profile_picture_attribute": "프로필 사진 속성",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "이 속성의 값은 URL, 바이너리 또는 base64 인코딩된 이미지일 수 있습니다.",
|
||||
"group_members_attribute": "그룹 멤버 속성",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "그룹의 멤버를 질의할 때 사용할 속성.",
|
||||
"group_unique_identifier_attribute": "그룹 고유 식별자 속성",
|
||||
"group_name_attribute": "그룹 멤버 속성",
|
||||
"admin_group_name": "관리자 그룹 이름",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "이 그룹의 멤버들은 Pocket ID에서 관리자 권한을 갖게 됩니다.",
|
||||
"disable": "비활성화",
|
||||
"sync_now": "지금 동기화",
|
||||
"enable": "활성화",
|
||||
"user_created_successfully": "사용자가 성공적으로 생성되었습니다",
|
||||
"create_user": "사용자 생성",
|
||||
"add_a_new_user_to_appname": "{appName}에 새로운 사용자를 추가하세요",
|
||||
"add_user": "사용자 추가",
|
||||
"manage_users": "사용자 관리",
|
||||
"admin_privileges": "관리자 권한",
|
||||
"admins_have_full_access_to_the_admin_panel": "관리자는 관리 패널에 대한 전체 접근 권한을 갖습니다.",
|
||||
"delete_firstname_lastname": "{firstName} {lastName} 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_user": "이 사용자를 삭제하시겠습니까?",
|
||||
"user_deleted_successfully": "사용자가 성공적으로 삭제되었습니다",
|
||||
"role": "역할",
|
||||
"source": "소스",
|
||||
"admin": "관리자",
|
||||
"user": "사용자",
|
||||
"local": "로컬",
|
||||
"toggle_menu": "메뉴 표시 전환",
|
||||
"edit": "편집",
|
||||
"user_groups_updated_successfully": "사용자 그룹이 성공적으로 업데이트되었습니다",
|
||||
"user_updated_successfully": "사용자가 성공적으로 업데이트되었습니다",
|
||||
"custom_claims_updated_successfully": "사용자 정의 클레임이 성공적으로 업데이트되었습니다",
|
||||
"back": "뒤로",
|
||||
"user_details_firstname_lastname": "{firstName} {lastName} 사용자 상세 정보",
|
||||
"manage_which_groups_this_user_belongs_to": "이 사용자가 속한 그룹을 관리합니다.",
|
||||
"custom_claims": "사용자 정의 클레임",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "사용자 정의 클레임은 사용자에 대한 추가 정보를 저장하기 위해 사용되는 키-값 쌍입니다. 이 클레임은 'profile' 스코프가 요청될 경우 ID 토큰에 포함됩니다.",
|
||||
"user_group_created_successfully": "사용자 그룹이 성공적으로 생성되었습니다",
|
||||
"create_user_group": "사용자 그룹 생성",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "사용자에게 할당할 수 있는 새로운 그룹을 생성합니다.",
|
||||
"add_group": "그룹 추가",
|
||||
"manage_user_groups": "사용자 그룹 관리",
|
||||
"friendly_name": "별칭",
|
||||
"name_that_will_be_displayed_in_the_ui": "UI에 표시되는 이름",
|
||||
"name_that_will_be_in_the_groups_claim": "\"groups\" 클레임에 표시되는 이름",
|
||||
"delete_name": "{name} 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "이 사용자 그룹을 삭제하시겠습니까?",
|
||||
"user_group_deleted_successfully": "사용자 그룹이 성공적으로 삭제되었습니다",
|
||||
"user_count": "사용자 수",
|
||||
"user_group_updated_successfully": "사용자 그룹이 성공적으로 업데이트되었습니다",
|
||||
"users_updated_successfully": "사용자가 성공적으로 업데이트되었습니다",
|
||||
"user_group_details_name": "{name} 사용자 그룹 상세 정보",
|
||||
"assign_users_to_this_group": "이 그룹에 사용자를 할당합니다.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "사용자 정의 클레임은 사용자에 대한 추가 정보를 저장하기 위해 사용되는 키-값 쌍입니다. 이 클레임은 'profile' 스코프가 요청될 경우 ID 토큰에 포함됩니다. 사용자 정의 클레임은 충돌이 있을 경우 우선 순위를 갖습니다.",
|
||||
"oidc_client_created_successfully": "OIDC 클라이언트가 성공적으로 생성되었습니다",
|
||||
"create_oidc_client": "OIDC 클라이언트 생성",
|
||||
"add_a_new_oidc_client_to_appname": "{appName}에 새로운 OIDC 클라이언트를 추가합니다.",
|
||||
"add_oidc_client": "OIDC 클라이언트 추가",
|
||||
"manage_oidc_clients": "OIDC 클라이언트 관리",
|
||||
"one_time_link": "일회용 링크",
|
||||
"use_this_link_to_sign_in_once": "이 링크를 사용하여 한 번 로그인하세요. 이 기능은 패스키를 추가하지 않았거나 패스키를 분실한 사용자에게 필요합니다.",
|
||||
"add": "추가",
|
||||
"callback_urls": "콜백 URL",
|
||||
"logout_callback_urls": "로그아웃 콜백 URL",
|
||||
"public_client": "공개 클라이언트",
|
||||
"public_clients_description": "공개 클라이언트는 클라이언트 시크릿이 없습니다. 이들은 시크릿을 안전하게 보관할 수 없는 모바일, 웹, 네이티브 애플리케이션을 위해 설계되었습니다.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "공개 키 코드 교환은 CSRF 및 승인 코드 가로채기 공격을 방지하기 위한 보안 기능입니다.",
|
||||
"requires_reauthentication": "재인증 요구",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "사용자가 이미 로그인한 상태에서도 승인할 때마다 다시 인증을 요구합니다.",
|
||||
"name_logo": "{name} 로고",
|
||||
"change_logo": "로고 변경",
|
||||
"upload_logo": "로고 업로드",
|
||||
"remove_logo": "로고 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "이 OIDC 클라이언트를 삭제하시겠습니까?",
|
||||
"oidc_client_deleted_successfully": "OIDC 클라이언트가 성공적으로 삭제되었습니다",
|
||||
"authorization_url": "승인 URL",
|
||||
"oidc_discovery_url": "OIDC 디스커버리 URL",
|
||||
"token_url": "토큰 URL",
|
||||
"userinfo_url": "사용자 정보 URL",
|
||||
"logout_url": "로그아웃 URL",
|
||||
"certificate_url": "인증서 URL",
|
||||
"enabled": "활성화됨",
|
||||
"disabled": "비활성화됨",
|
||||
"oidc_client_updated_successfully": "OIDC 클라이언트가 성공적으로 업데이트되었습니다",
|
||||
"create_new_client_secret": "새로운 클라이언트 시크릿 생성",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "새로운 클라이언트 시크릿 생성하시겠습니까? 기존 클라이언트 시크릿은 무효화됩니다.",
|
||||
"generate": "생성",
|
||||
"new_client_secret_created_successfully": "새로운 클라이언트 시크릿이 성공적으로 생성되었습니다",
|
||||
"allowed_user_groups_updated_successfully": "허용된 사용자 그룹이 성공적으로 업데이트되었습니다",
|
||||
"oidc_client_name": "OIDC 클라이언트 {name}",
|
||||
"client_id": "클라이언트 ID",
|
||||
"client_secret": "클라이언트 시크릿",
|
||||
"show_more_details": "상세 정보 보기",
|
||||
"allowed_user_groups": "허용된 사용자 그룹",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "이 클라이언트에 사용자 그룹을 추가하여 해당 그룹의 사용자의 접근를 제한합니다. 사용자 그룹을 선택하지 않으면 모든 사용자가 이 클라이언트에 접근할 수 있습니다.",
|
||||
"favicon": "파비콘",
|
||||
"light_mode_logo": "라이트 모드 로고",
|
||||
"dark_mode_logo": "다크 모드 로고",
|
||||
"background_image": "배경 이미지",
|
||||
"language": "언어",
|
||||
"reset_profile_picture_question": "프로필 사진을 재설정하시겠습니까?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "업로드한 이미지를 삭제하고 프로필 이미지를 기본값으로 되돌립니다. 계속하시겠습니까?",
|
||||
"reset": "재설정",
|
||||
"reset_to_default": "기본값으로 재설정",
|
||||
"profile_picture_has_been_reset": "프로필 사진이 재설정되었습니다. 업데이트에 몇 분 정도 소요될 수 있습니다.",
|
||||
"select_the_language_you_want_to_use": "사용할 언어를 선택하세요. 일부 텍스트는 자동으로 번역되었을 수 있으며, 정확하지 않을 수 있습니다.",
|
||||
"contribute_to_translation": "문제를 발견했다면 <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>에서 번역에 기여해 주세요.",
|
||||
"personal": "개인",
|
||||
"global": "전역",
|
||||
"all_users": "모든 사용자",
|
||||
"all_events": "모든 이벤트",
|
||||
"all_clients": "모든 클라이언트",
|
||||
"all_locations": "모든 위치",
|
||||
"global_audit_log": "전역 감사 로그",
|
||||
"see_all_account_activities_from_the_last_3_months": "지난 3개월 동안의 모든 사용자 활동을 확인하세요.",
|
||||
"token_sign_in": "토큰 로그인",
|
||||
"client_authorization": "클라이언트 승인",
|
||||
"new_client_authorization": "새로운 클라이언트 승인",
|
||||
"disable_animations": "애니메이션 비활성화",
|
||||
"turn_off_ui_animations": "UI 전체의 애니메이션을 비활성화합니다.",
|
||||
"user_disabled": "계정 비활성화",
|
||||
"disabled_users_cannot_log_in_or_use_services": "비활성화된 사용자는 로그인하거나 서비스를 사용할 수 없습니다.",
|
||||
"user_disabled_successfully": "사용자가 성공적으로 비활성화되었습니다.",
|
||||
"user_enabled_successfully": "사용자가 성공적으로 활성화되었습니다.",
|
||||
"status": "상태",
|
||||
"disable_firstname_lastname": "{firstName} {lastName} 비활성화",
|
||||
"are_you_sure_you_want_to_disable_this_user": "이 사용자를 비활성화하시겠습니까? 이 사용자는 로그인하거나 어떤 서비스에도 접근할 수 없게 됩니다.",
|
||||
"ldap_soft_delete_users": "LDAP에 비활성화된 사용자 유지",
|
||||
"ldap_soft_delete_users_description": "이 기능이 활성화되면 LDAP에서 삭제된 사용자는 시스템에서 삭제되지 않고 비활성화됩니다.",
|
||||
"login_code_email_success": "로그인 코드가 사용자에게 전송되었습니다.",
|
||||
"send_email": "이메일 전송",
|
||||
"show_code": "코드 표시",
|
||||
"callback_url_description": "클라이언트가 제공한 URL. 비워둔 경우 자동으로 추가됩니다. 와일드카드(*)도 지원하지만, 보안상의 이유로 사용을 권장하지 않습니다.",
|
||||
"logout_callback_url_description": "클라이언트가 제공한 로그아웃 URL. 와일드카드(*)도 지원하지만, 보안상의 이유로 사용을 권장하지 않습니다.",
|
||||
"api_key_expiration": "API 키 만료",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API 키가 만료되기 전에 사용자에게 이메일을 전송합니다.",
|
||||
"authorize_device": "기기 승인",
|
||||
"the_device_has_been_authorized": "기기가 승인되었습니다.",
|
||||
"enter_code_displayed_in_previous_step": "이전 단계에 표시된 코드를 입력하세요.",
|
||||
"authorize": "승인",
|
||||
"federated_client_credentials": "연동 클라이언트 자격 증명",
|
||||
"federated_client_credentials_description": "연동 클라이언트 자격 증명을 이용하여, OIDC 클라이언트를 제3자 인증 기관에서 발급한 JWT 토큰을 이용해 인증할 수 있습니다.",
|
||||
"add_federated_client_credential": "연동 클라이언트 자격 증명 추가",
|
||||
"add_another_federated_client_credential": "다른 연동 클라이언트 자격 증명 추가",
|
||||
"oidc_allowed_group_count": "허용된 그룹 수",
|
||||
"unrestricted": "제한 없음",
|
||||
"show_advanced_options": "고급 옵션 표시",
|
||||
"hide_advanced_options": "고급 옵션 숨기기",
|
||||
"oidc_data_preview": "OIDC 데이터 미리보기",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "여러 사용자를 위해 전송될 OIDC 데이터 미리보기",
|
||||
"id_token": "ID 토큰",
|
||||
"access_token": "접근 토큰",
|
||||
"userinfo": "사용자 정보",
|
||||
"id_token_payload": "ID 토큰 페이로드",
|
||||
"access_token_payload": "접근 토큰 페이로드",
|
||||
"userinfo_endpoint_response": "사용자 정보 엔드포인트 응답",
|
||||
"copy": "복사",
|
||||
"no_preview_data_available": "미리보기 데이터가 없습니다",
|
||||
"copy_all": "모두 복사",
|
||||
"preview": "미리보기",
|
||||
"preview_for_user": "{name} ({email}) 미리보기",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "이 사용자를 위해 전송될 OIDC 데이터 미리보기",
|
||||
"show": "표시",
|
||||
"select_an_option": "옵션 선택",
|
||||
"select_user": "사용자 선택",
|
||||
"error": "오류",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Pocket ID의 외관을 맞춤 설정하려면 강조 색상을 선택하세요.",
|
||||
"accent_color": "강조 색상",
|
||||
"custom_accent_color": "맞춤 강조 색상",
|
||||
"custom_accent_color_description": "유효한 CSS 색상 형식(예: hex, rgb, hsl)을 사용하여 맞춤 색상을 입력하세요.",
|
||||
"color_value": "색상 값",
|
||||
"apply": "적용",
|
||||
"signup_token": "계정 생성 토큰",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "새로운 사용자 등록을 허용하기 위해 계정 생성 토큰을 생성합니다.",
|
||||
"usage_limit": "사용량 제한",
|
||||
"number_of_times_token_can_be_used": "계정 생성 토큰을 사용할 수 있는 횟수.",
|
||||
"expires": "만료일",
|
||||
"signup": "계정 생성",
|
||||
"user_creation": "사용자 생성",
|
||||
"configure_user_creation": "사용자 생성 설정을 관리합니다. 이에는 신규 사용자 등록 방법 및 신규 사용자의 기본 권한이 포함됩니다.",
|
||||
"user_creation_groups_description": "새 사용자가 가입할 때 이 그룹을 자동으로 할당합니다.",
|
||||
"user_creation_claims_description": "새 사용자가 가입할 때 이 사용자 정의 클레임을 자동으로 할당합니다.",
|
||||
"user_creation_updated_successfully": "사용자 생성 설정 업데이트가 성공적으로 완료되었습니다.",
|
||||
"signup_disabled_description": "계정 생성이 완전히 비활성화됩니다. 새로운 사용자 계정은 관리자만 생성할 수 있습니다.",
|
||||
"signup_requires_valid_token": "계정을 생성하려면 유효한 계정 생성 토큰이 필요합니다",
|
||||
"validating_signup_token": "계정 생성 토큰 검증",
|
||||
"go_to_login": "로그인으로 이동",
|
||||
"signup_to_appname": "{appName} 계정 생성하기",
|
||||
"create_your_account_to_get_started": "계정을 만들어 시작하세요.",
|
||||
"initial_account_creation_description": "시작하기 위해 계정을 만드세요. 패스키를 나중에 설정할 수 있습니다.",
|
||||
"setup_your_passkey": "패스키 설정",
|
||||
"create_a_passkey_to_securely_access_your_account": "계정에 안전하게 접근하기 위해 패스키를 생성하세요. 이 패스키는 로그인을 위한 주요 방법으로 사용됩니다.",
|
||||
"skip_for_now": "지금은 건너뛰기",
|
||||
"account_created": "계정 생성됨",
|
||||
"enable_user_signups": "계정 생성 활성화",
|
||||
"enable_user_signups_description": "Pocket ID에서 사용자가 새로운 계정을 생성하는 방법을 결정하세요.",
|
||||
"user_signups_are_disabled": "계정 생성이 현재 비활성화되었습니다",
|
||||
"create_signup_token": "계정 생성 토큰 생성",
|
||||
"view_active_signup_tokens": "활성 계정 생성 토큰 보기",
|
||||
"manage_signup_tokens": "계정 생성 토큰 관리",
|
||||
"view_and_manage_active_signup_tokens": "활성 계정 생성 토큰을 조회하고 관리합니다.",
|
||||
"signup_token_deleted_successfully": "계정 생성 토큰이 성공적으로 삭제되었습니다.",
|
||||
"expired": "만료됨",
|
||||
"used_up": "사용 완료",
|
||||
"active": "활성",
|
||||
"usage": "사용량",
|
||||
"created": "생성일",
|
||||
"token": "토큰",
|
||||
"loading": "불러오는 중",
|
||||
"delete_signup_token": "계정 생성 토큰 삭제",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "이 계정 생성 토큰을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||
"signup_with_token": "토큰으로 계정 생성",
|
||||
"signup_with_token_description": "사용자는 관리자가 생성한 유효한 계정 생성 토큰을 사용해야 가입할 수 있습니다.",
|
||||
"signup_open": "계정 생성 허용",
|
||||
"signup_open_description": "누구나 제한 없이 새로운 계정을 생성할 수 있습니다.",
|
||||
"of": "의",
|
||||
"skip_passkey_setup": "패스키 설정 건너뛰기",
|
||||
"skip_passkey_setup_description": "패스키를 설정하는 것이 강력히 권장됩니다. 패스키가 없으면 세션이 만료되자마자 계정에 접근할 수 없게 됩니다.",
|
||||
"my_apps": "내 앱",
|
||||
"no_apps_available": "사용 가능한 앱이 없습니다",
|
||||
"contact_your_administrator_for_app_access": "관리자에게 연락하여 앱의 접근 권한을 얻으세요.",
|
||||
"launch": "실행",
|
||||
"client_launch_url": "클라이언트 실행 URL",
|
||||
"client_launch_url_description": "사용자가 '내 앱' 페이지에서 앱을 실행할 때 열릴 URL입니다.",
|
||||
"client_name_description": "Pocket ID UI에 표시되는 클라이언트의 이름입니다.",
|
||||
"revoke_access": "접근 권한 취소",
|
||||
"revoke_access_description": "<b>{clientName}</b>의 접근 권한을 취소합니다. <b>{clientName}</b>은 더 이상 계정 정보에 접근할 수 없습니다.",
|
||||
"revoke_access_successful": "{clientName}의 접근이 성공적으로 취소되었습니다.",
|
||||
"last_signed_in_ago": "{time} 전에 로그인함",
|
||||
"invalid_client_id": "고객 ID에는 영문자, 숫자, 밑줄, 하이픈만 포함될 수 있습니다.",
|
||||
"custom_client_id_description": "애플리케이션에서 사용자 정의 클라이언트 ID가 요구되는 경우 설정하세요. 그렇지 않으면 빈 상태로 두어서 무작위로 생성할 수 있습니다.",
|
||||
"generated": "생성됨"
|
||||
}
|
||||
@@ -3,17 +3,17 @@
|
||||
"my_account": "Mijn account",
|
||||
"logout": "Uitloggen",
|
||||
"confirm": "Bevestigen",
|
||||
"docs": "Documenten",
|
||||
"docs": "Documentatie",
|
||||
"key": "Sleutel",
|
||||
"value": "Waarde",
|
||||
"remove_custom_claim": "Aangepaste claim verwijderen",
|
||||
"add_custom_claim": "Aangepaste claim toevoegen",
|
||||
"add_another": "Voeg er nog een toe",
|
||||
"add_another": "Nog een toevoegen",
|
||||
"select_a_date": "Selecteer een datum",
|
||||
"select_file": "Selecteer bestand",
|
||||
"profile_picture": "Profielfoto",
|
||||
"profile_picture_is_managed_by_ldap_server": "De profielfoto wordt beheerd door de LDAP-server en kan hier niet worden gewijzigd.",
|
||||
"click_profile_picture_to_upload_custom": "Klik op de profielfoto om een aangepaste foto uit uw bestanden te uploaden.",
|
||||
"click_profile_picture_to_upload_custom": "Klik op de profielfoto om een aangepaste foto uit je bestanden te uploaden.",
|
||||
"image_should_be_in_format": "De afbeelding moet in PNG- of JPEG-formaat zijn.",
|
||||
"items_per_page": "Aantal per pagina",
|
||||
"no_items_found": "Geen items gevonden",
|
||||
@@ -22,8 +22,8 @@
|
||||
"copied": "Gekopieerd",
|
||||
"click_to_copy": "Klik om te kopiëren",
|
||||
"something_went_wrong": "Er is iets misgegaan",
|
||||
"go_back_to_home": "Ga terug naar huis",
|
||||
"dont_have_access_to_your_passkey": "Heeft u geen toegang tot uw toegangscode?",
|
||||
"go_back_to_home": "Terug naar beginpagina",
|
||||
"alternative_sign_in_methods": "Andere manieren om in te loggen",
|
||||
"login_background": "Inlogachtergrond",
|
||||
"logo": "Logo",
|
||||
"login_code": "Inlogcode",
|
||||
@@ -42,49 +42,49 @@
|
||||
"authentication_process_was_aborted": "Het authenticatieproces is afgebroken",
|
||||
"error_occurred_with_authenticator": "Er is een fout opgetreden met de authenticator",
|
||||
"authenticator_does_not_support_discoverable_credentials": "De authenticator ondersteunt geen vindbare referenties",
|
||||
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen residente sleutels",
|
||||
"passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd",
|
||||
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen vaste sleutels",
|
||||
"passkey_was_previously_registered": "Deze passkey is eerder geregistreerd",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen",
|
||||
"authenticator_timed_out": "De authenticator is verlopen",
|
||||
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met uw beheerder.",
|
||||
"sign_in_to": "Meld u aan bij {name}",
|
||||
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met de beheerder.",
|
||||
"sign_in_to": "Meld je aan bij {name}",
|
||||
"client_not_found": "Client niet gevonden",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wil toegang tot de volgende informatie:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Wilt u zich aanmelden bij <b>{client}</b> met uw {appName} account?",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Wil je je aanmelden bij <b>{client}</b> met je {appName} account?",
|
||||
"email": "E-mail",
|
||||
"view_your_email_address": "Bekijk uw e-mailadres",
|
||||
"view_your_email_address": "Bekijk je e-mailadres",
|
||||
"profile": "Profiel",
|
||||
"view_your_profile_information": "Bekijk uw profielgegevens",
|
||||
"view_your_profile_information": "Bekijk je profielgegevens",
|
||||
"groups": "Groepen",
|
||||
"view_the_groups_you_are_a_member_of": "Bekijk de groepen waarvan u lid bent",
|
||||
"view_the_groups_you_are_a_member_of": "Bekijk de groepen waarvan je lid bent",
|
||||
"cancel": "Annuleren",
|
||||
"sign_in": "Aanmelden",
|
||||
"try_again": "Probeer het opnieuw",
|
||||
"client_logo": "Client logo",
|
||||
"sign_out": "Afmelden",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Wilt u zich afmelden bij Pocket ID met het account <b>{username}</b> ?",
|
||||
"sign_in_to_appname": "Meld u aan bij {appName}",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Wil je je afmelden bij {appName} met het account <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Meld je aan bij {appName}",
|
||||
"please_try_to_sign_in_again": "Probeer opnieuw in te loggen.",
|
||||
"authenticate_with_passkey_to_access_account": "Log in met je passkey om toegang te krijgen tot je account.",
|
||||
"authenticate_with_passkey_to_access_account": "Log in met uw passkey om toegang te krijgen tot uw account.",
|
||||
"authenticate": "Authenticeren",
|
||||
"please_try_again": "Probeer het opnieuw.",
|
||||
"continue": "Doorgaan",
|
||||
"alternative_sign_in": "Alternatieve aanmelding",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Als u geen toegang hebt tot uw passkeys, kunt u zich op een van de volgende manieren aanmelden.",
|
||||
"use_your_passkey_instead": "Wilt u in plaats daarvan uw toegangscode gebruiken?",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Als je geen toegang hebt tot je passkey, kun je je op een van de volgende manieren aanmelden.",
|
||||
"use_your_passkey_instead": "Wil je in plaats daarvan je passkey gebruiken?",
|
||||
"email_login": "E-mail inloggen",
|
||||
"enter_a_login_code_to_sign_in": "Voer een inlogcode in om in te loggen.",
|
||||
"request_a_login_code_via_email": "Vraag een inlogcode aan via e-mail.",
|
||||
"go_back": "Ga terug",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Er is een e-mail verzonden naar het opgegeven e-mailadres, indien dit in het systeem voorkomt.",
|
||||
"go_back": "Terug",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Er is een e-mail verzonden naar het opgegeven e-mailadres, indien deze in het systeem voorkomt.",
|
||||
"enter_code": "Voer code in",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Voer uw e-mailadres in om een e-mail met een inlogcode te ontvangen.",
|
||||
"your_email": "Uw e-mail",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Voer je e-mailadres in om een e-mail met een inlogcode te ontvangen.",
|
||||
"your_email": "Je e-mail",
|
||||
"submit": "Indienen",
|
||||
"enter_the_code_you_received_to_sign_in": "Voer de code in die u hebt ontvangen om in te loggen.",
|
||||
"enter_the_code_you_received_to_sign_in": "Voer de code in die je hebt ontvangen om in te loggen.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Ongeldige omleidings-URL",
|
||||
"audit_log": "Audit logboek",
|
||||
"audit_log": "Activiteitenlogboek",
|
||||
"users": "Gebruikers",
|
||||
"user_groups": "Gebruikersgroepen",
|
||||
"oidc_clients": "OIDC-clients",
|
||||
@@ -93,52 +93,52 @@
|
||||
"settings": "Instellingen",
|
||||
"update_pocket_id": "Pocket-ID bijwerken",
|
||||
"powered_by": "Aangedreven door",
|
||||
"see_your_account_activities_from_the_last_3_months": "Bekijk uw accountactiviteiten van de afgelopen 3 maanden.",
|
||||
"see_your_account_activities_from_the_last_3_months": "Bekijk je accountactiviteiten van de afgelopen 3 maanden.",
|
||||
"time": "Tijd",
|
||||
"event": "Evenement",
|
||||
"event": "Activiteit",
|
||||
"approximate_location": "Geschatte locatie",
|
||||
"ip_address": "IP-adres",
|
||||
"device": "Apparaat",
|
||||
"client": "Cliënt",
|
||||
"client": "Client",
|
||||
"unknown": "Onbekend",
|
||||
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
|
||||
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||
"account_settings": "Accountinstellingen",
|
||||
"passkey_missing": "Passkey ontbreekt",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat u de toegang tot uw account verliest.",
|
||||
"single_passkey_configured": "Eén enkele toegangscode geconfigureerd",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat u de toegang tot uw account verliest.",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat je de toegang tot je account verliest.",
|
||||
"single_passkey_configured": "Eén enkele passkey geconfigureerd",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één passkey toe te voegen om te voorkomen dat je de toegang tot uw account verliest.",
|
||||
"account_details": "Accountgegevens",
|
||||
"passkeys": "Toegangscodes",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de passkeys waarmee je jezelf kunt verifiëren.",
|
||||
"add_passkey": "Passkey toevoegen",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Maak een eenmalige inlogcode aan om in te loggen vanaf een ander apparaat zonder passkey.",
|
||||
"create": "Creëren",
|
||||
"create": "Aanmaken",
|
||||
"first_name": "Voornaam",
|
||||
"last_name": "Achternaam",
|
||||
"username": "Gebruikersnaam",
|
||||
"save": "Opslaan",
|
||||
"username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld u aan met de volgende code. De code verloopt over 15 minuten.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld je aan met de volgende code. De code verloopt over 15 minuten.",
|
||||
"or_visit": "of bezoek",
|
||||
"added_on": "Toegevoegd op",
|
||||
"rename": "Hernoemen",
|
||||
"delete": "Verwijderen",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Weet u zeker dat u deze toegangscode wilt verwijderen?",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Weet je zeker dat je deze passkey wilt verwijderen?",
|
||||
"passkey_deleted_successfully": "Passkey succesvol verwijderd",
|
||||
"delete_passkey_name": "Verwijder {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt",
|
||||
"name_passkey": "Naam Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Geef uw toegangscode een naam, zodat u deze later gemakkelijk kunt terugvinden.",
|
||||
"name_passkey": "Naam passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Geef je passkey een naam, zodat je deze later gemakkelijk kunt terugvinden.",
|
||||
"create_api_key": "API-sleutel aanmaken",
|
||||
"add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.",
|
||||
"add_api_key": "API-sleutel toevoegen",
|
||||
"manage_api_keys": "API-sleutels beheren",
|
||||
"api_key_created": "API-sleutel gemaakt",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Om veiligheidsredenen wordt deze sleutel slechts één keer getoond. Bewaar hem veilig.",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Om veiligheidsredenen wordt deze sleutel slechts één keer getoond. Bewaar deze veilig.",
|
||||
"description": "Beschrijving",
|
||||
"api_key": "API-sleutel",
|
||||
"close": "Dichtbij",
|
||||
"close": "Sluiten",
|
||||
"name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.",
|
||||
"expires_at": "Verloopt op",
|
||||
"when_this_api_key_will_expire": "Wanneer deze API-sleutel verloopt.",
|
||||
@@ -146,30 +146,30 @@
|
||||
"expiration_date_must_be_in_the_future": "Vervaldatum moet in de toekomst liggen",
|
||||
"revoke_api_key": "API-sleutel intrekken",
|
||||
"never": "Nooit",
|
||||
"revoke": "Herroepen",
|
||||
"revoke": "Intrekken",
|
||||
"api_key_revoked_successfully": "API-sleutel succesvol ingetrokken",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet u zeker dat u 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 u de API-sleutel \"{apiKeyName}\" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.",
|
||||
"last_used": "Laatst gebruikt",
|
||||
"actions": "Acties",
|
||||
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
||||
"general": "Algemeen",
|
||||
"configure_smtp_to_send_emails": "Zet e-mailmeldingen aan om mensen 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",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configureer LDAP-instellingen om gebruikers en groepen vanaf een LDAP-server te synchroniseren.",
|
||||
"images": "Afbeeldingen",
|
||||
"update": "Update",
|
||||
"email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt",
|
||||
"save_changes_question": "Wijzigingen opslaan?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "U moet de wijzigingen opslaan voordat u een test-e-mail verzendt. Wilt u nu opslaan?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Je moet de wijzigingen opslaan voordat je een test-e-mail verzendt. Wil je nu opslaan?",
|
||||
"save_and_send": "Opslaan en verzenden",
|
||||
"test_email_sent_successfully": "Test-e-mail succesvol verzonden naar uw e-mailadres.",
|
||||
"test_email_sent_successfully": "Test-e-mail succesvol verzonden naar je e-mailadres.",
|
||||
"failed_to_send_test_email": "Het is niet gelukt om een test-e-mail te versturen. Controleer de serverlogs voor meer informatie.",
|
||||
"smtp_configuration": "SMTP-configuratie",
|
||||
"smtp_host": "SMTP-host",
|
||||
"smtp_port": "SMTP-poort",
|
||||
"smtp_user": "SMTP-gebruiker",
|
||||
"smtp_password": "SMTP-wachtwoord",
|
||||
"smtp_from": "SMTP van",
|
||||
"smtp_from": "SMTP-afzender",
|
||||
"smtp_tls_option": "SMTP TLS-optie",
|
||||
"email_tls_option": "E-mail TLS-optie",
|
||||
"skip_certificate_verification": "Certificaatverificatie overslaan",
|
||||
@@ -178,15 +178,15 @@
|
||||
"email_login_notification": "E-mail-inlogmelding",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Stuur een e-mail naar de gebruiker wanneer deze zich aanmeldt vanaf een nieuw apparaat.",
|
||||
"emai_login_code_requested_by_user": "E-mail login code aangevraagd door gebruiker",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers wachtwoorden omzeilen door een inlogcode aan te vragen die naar hun e-mail wordt gestuurd. Dit maakt het een stuk minder veilig, omdat iedereen die toegang heeft tot de e-mail van de gebruiker binnen kan komen.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers passkeys omzeilen door een inlogcode aan te vragen die naar hun e-mail wordt gestuurd. Dit maakt het inloggen een stuk minder veilig, omdat iedereen die toegang heeft tot de e-mail van de gebruiker binnen kan komen.",
|
||||
"email_login_code_from_admin": "E-mail inlogcode van beheerder",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Hiermee kan een admin een inlogcode naar de gebruiker mailen.",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Hiermee kan een beheerder een inlogcode naar de gebruiker mailen.",
|
||||
"send_test_email": "Test-e-mail verzenden",
|
||||
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
||||
"application_name": "Toepassingsnaam",
|
||||
"session_duration": "Sessieduur",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "De duur van een sessie in minuten voordat de gebruiker zich opnieuw moet aanmelden.",
|
||||
"enable_self_account_editing": "Zelf-accountbewerking inschakelen",
|
||||
"enable_self_account_editing": "Bewerken van eigen account mogelijk maken",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Of gebruikers hun eigen accountgegevens moeten kunnen bewerken.",
|
||||
"emails_verified": "E-mails geverifieerd",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Of het e-mailadres van de gebruiker als geverifieerd moet worden gemarkeerd voor de OIDC-clients.",
|
||||
@@ -198,26 +198,26 @@
|
||||
"ldap_bind_dn": "LDAP Bind-DN",
|
||||
"ldap_bind_password": "LDAP Bind-wachtwoord",
|
||||
"ldap_base_dn": "LDAP-basis-DN",
|
||||
"user_search_filter": "Gebruikerszoekfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee u gebruikers kunt zoeken/synchroniseren.",
|
||||
"groups_search_filter": "Groepen Zoekfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee u groepen kunt zoeken/synchroniseren.",
|
||||
"user_search_filter": "Zoekfilter gebruikers",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee je gebruikers kunt zoeken/synchroniseren.",
|
||||
"groups_search_filter": "Zoekfilter groepen",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee je groepen kunt zoeken/synchroniseren.",
|
||||
"attribute_mapping": "Attribuuttoewijzing",
|
||||
"user_unique_identifier_attribute": "Gebruiker uniek identificatiekenmerk",
|
||||
"the_value_of_this_attribute_should_never_change": "De waarde van dit kenmerk mag nooit veranderen.",
|
||||
"username_attribute": "Gebruikersnaam Attribuut",
|
||||
"user_mail_attribute": "Gebruikersmailkenmerk",
|
||||
"user_first_name_attribute": "Gebruikersvoornaam Attribuut",
|
||||
"user_last_name_attribute": "Gebruikersnaam Achternaam Attribuut",
|
||||
"user_profile_picture_attribute": "Gebruikersprofielfoto-attribuut",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "De waarde van dit kenmerk kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.",
|
||||
"group_members_attribute": "Groepsleden Attribuut",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Het kenmerk dat gebruikt moet worden om leden van een groep te bevragen.",
|
||||
"group_unique_identifier_attribute": "Groeps uniek identificatiekenmerk",
|
||||
"group_name_attribute": "Groepsnaam Attribuut",
|
||||
"user_unique_identifier_attribute": "Uniek gebruikersidentificatie attribuut",
|
||||
"the_value_of_this_attribute_should_never_change": "De waarde van dit attribuut mag nooit veranderen.",
|
||||
"username_attribute": "Gebruikersnaam attribuut",
|
||||
"user_mail_attribute": "Gebruikers e-mail attribuut",
|
||||
"user_first_name_attribute": "Gebruikers voornaam attribuut",
|
||||
"user_last_name_attribute": "Gebruikers achternaam attribuut",
|
||||
"user_profile_picture_attribute": "Gebruikers profielfoto attribuut",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "De waarde van dit attribuut kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.",
|
||||
"group_members_attribute": "Groepsleden attribuut",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Het attribuut dat gebruikt moet worden om leden van een groep te bevragen.",
|
||||
"group_unique_identifier_attribute": "Uniek groepsidentificatie attribuut",
|
||||
"group_name_attribute": "Groepsnaam attribuut",
|
||||
"admin_group_name": "Naam van beheerdersgroep",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Leden van deze groep hebben beheerdersrechten in Pocket ID.",
|
||||
"disable": "Uitzetten",
|
||||
"disable": "Uitschakelen",
|
||||
"sync_now": "Nu synchroniseren",
|
||||
"enable": "Inschakelen",
|
||||
"user_created_successfully": "Gebruiker succesvol aangemaakt",
|
||||
@@ -228,7 +228,7 @@
|
||||
"admin_privileges": "Beheerdersrechten",
|
||||
"admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.",
|
||||
"delete_firstname_lastname": "Verwijderen {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Weet u zeker dat u deze gebruiker wilt verwijderen?",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Weet je zeker dat u deze gebruiker wilt verwijderen?",
|
||||
"user_deleted_successfully": "Gebruiker succesvol verwijderd",
|
||||
"role": "Rol",
|
||||
"source": "Bron",
|
||||
@@ -236,7 +236,7 @@
|
||||
"user": "Gebruiker",
|
||||
"local": "Lokaal",
|
||||
"toggle_menu": "Menu wisselen",
|
||||
"edit": "Bewerking",
|
||||
"edit": "Bewerk",
|
||||
"user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt",
|
||||
"user_updated_successfully": "Gebruiker succesvol bijgewerkt",
|
||||
"custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt",
|
||||
@@ -252,9 +252,9 @@
|
||||
"manage_user_groups": "Gebruikersgroepen beheren",
|
||||
"friendly_name": "Vriendelijke naam",
|
||||
"name_that_will_be_displayed_in_the_ui": "Naam die in de gebruikersinterface wordt weergegeven",
|
||||
"name_that_will_be_in_the_groups_claim": "Naam die in de claim 'groepen' zal staan",
|
||||
"name_that_will_be_in_the_groups_claim": "Naam die in de claim 'groups' zal staan",
|
||||
"delete_name": "Verwijder {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Weet u zeker dat u deze gebruikersgroep wilt verwijderen?",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Weet je zeker dat je deze gebruikersgroep wilt verwijderen?",
|
||||
"user_group_deleted_successfully": "Gebruikersgroep succesvol verwijderd",
|
||||
"user_count": "Gebruikersaantal",
|
||||
"user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt",
|
||||
@@ -268,19 +268,21 @@
|
||||
"add_oidc_client": "OIDC-client toevoegen",
|
||||
"manage_oidc_clients": "OIDC-clients beheren",
|
||||
"one_time_link": "Eenmalige link",
|
||||
"use_this_link_to_sign_in_once": "Gebruik deze link om u eenmalig aan te melden. Dit is nodig voor gebruikers die nog geen passkey hebben toegevoegd of deze kwijt zijn.",
|
||||
"use_this_link_to_sign_in_once": "Gebruik deze link om eenmalig aan te melden. Dit is nodig voor gebruikers die nog geen passkey hebben toegevoegd of deze kwijt zijn.",
|
||||
"add": "Toevoegen",
|
||||
"callback_urls": "Callback-URL's",
|
||||
"logout_callback_urls": "Callback-URL's voor afmelden",
|
||||
"public_client": "Publieke client",
|
||||
"public_clients_description": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als uw client een SPA of mobiele app is.",
|
||||
"public_clients_description": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als je client een SPA of mobiele app is.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.",
|
||||
"requires_reauthentication": "Je moet opnieuw inloggen",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Gebruikers moeten bij elke autorisatie opnieuw inloggen, zelfs als ze al ingelogd zijn.",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Logo wijzigen",
|
||||
"upload_logo": "Logo uploaden",
|
||||
"remove_logo": "Logo verwijderen",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Weet u zeker dat u deze OIDC-client wilt verwijderen?",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Weet je zeker dat je deze OIDC-client wilt verwijderen?",
|
||||
"oidc_client_deleted_successfully": "OIDC-client succesvol verwijderd",
|
||||
"authorization_url": "Autorisatie-URL",
|
||||
"oidc_discovery_url": "OIDC-ontdekkings-URL",
|
||||
@@ -292,12 +294,12 @@
|
||||
"disabled": "Uitgeschakeld",
|
||||
"oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt",
|
||||
"create_new_client_secret": "Nieuw clientgeheim aanmaken",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Weet u zeker dat u een nieuw client secret wilt aanmaken? De oude wordt ongeldig.",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Weet je zeker dat je een nieuw client secret wilt aanmaken? De oude wordt ongeldig.",
|
||||
"generate": "Genereren",
|
||||
"new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt",
|
||||
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
|
||||
"oidc_client_name": "OIDC-client {name}",
|
||||
"client_id": "Client id",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client geheim",
|
||||
"show_more_details": "Meer details weergeven",
|
||||
"allowed_user_groups": "Toegestane gebruikersgroepen",
|
||||
@@ -308,50 +310,50 @@
|
||||
"background_image": "Achtergrondfoto",
|
||||
"language": "Taal",
|
||||
"reset_profile_picture_question": "Profielfoto opnieuw instellen?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Hiermee wordt de geüploade afbeelding verwijderd en wordt de profielfoto teruggezet naar de standaardinstelling. Wil je doorgaan?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Hiermee wordt de geüploade afbeelding verwijderd en wordt de profielfoto teruggezet naar de standaardinstelling. Wilt u doorgaan?",
|
||||
"reset": "Opnieuw instellen",
|
||||
"reset_to_default": "Standaardinstellingen herstellen",
|
||||
"profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||
"select_the_language_you_want_to_use": "Kies de taal die je wilt gebruiken. Let op: sommige teksten worden automatisch vertaald en kunnen onnauwkeurig zijn.",
|
||||
"contribute_to_translation": "Als je een fout vindt, kun je altijd helpen met de vertaling op <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"select_the_language_you_want_to_use": "Kies de taal die u wilt gebruiken. Let op: sommige teksten worden automatisch vertaald en kunnen onnauwkeurig zijn.",
|
||||
"contribute_to_translation": "Als u een fout vindt, kun je altijd helpen met de vertaling op <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Persoonlijk",
|
||||
"global": "Globaal",
|
||||
"all_users": "Alle gebruikers",
|
||||
"all_events": "Alle activiteiten",
|
||||
"all_clients": "Alle clients",
|
||||
"all_locations": "Alle locaties",
|
||||
"global_audit_log": "Algemeen audit logboek",
|
||||
"global_audit_log": "Globaal activiteitenlogboek",
|
||||
"see_all_account_activities_from_the_last_3_months": "Bekijk alle gebruikersactiviteit van de afgelopen 3 maanden.",
|
||||
"token_sign_in": "Inloggen met token",
|
||||
"client_authorization": "Client autorisatie",
|
||||
"new_client_authorization": "Nieuwe clientautorisatie",
|
||||
"disable_animations": "Animatie uitzetten",
|
||||
"disable_animations": "Animaties uitzetten",
|
||||
"turn_off_ui_animations": "Zet alle animaties in de gebruikersinterface uit.",
|
||||
"user_disabled": "Account uitgeschakeld",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Gebruikers met een handicap kunnen niet inloggen of diensten gebruiken.",
|
||||
"user_disabled_successfully": "Je bent nu uitgelogd.",
|
||||
"user_enabled_successfully": "Je bent nu aangemeld.",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Uitgeschakelde gebruikers kunnen niet inloggen of diensten gebruiken.",
|
||||
"user_disabled_successfully": "Gebruiker is succesvol uitgeschakeld.",
|
||||
"user_enabled_successfully": "Gebruiker is succesvol geactiveerd.",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "{firstName} {lastName}uitschakelen",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Weet je zeker dat je deze gebruiker wilt uitschakelen? Ze kunnen dan niet meer inloggen of diensten gebruiken.",
|
||||
"ldap_soft_delete_users": "Voorkom dat gebruikers met een handicap toegang krijgen tot LDAP.",
|
||||
"ldap_soft_delete_users_description": "Als dit is ingeschakeld, worden gebruikers die uit LDAP worden verwijderd, uitgeschakeld in plaats van uit het systeem verwijderd.",
|
||||
"login_code_email_success": "De inlogcode is naar je gestuurd.",
|
||||
"send_email": "E-mail sturen",
|
||||
"show_code": "Code tonen",
|
||||
"callback_url_description": "URL's die je klant heeft gegeven. Als je dit leeg laat, worden ze automatisch toegevoegd. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je die beter niet doen.",
|
||||
"logout_callback_url_description": "URL's die je klant heeft gegeven om uit te loggen. Je kunt jokertekens (*) gebruiken, maar dat is niet zo'n goed idee voor de veiligheid.",
|
||||
"disable_firstname_lastname": "{firstName} {lastName} uitschakelen",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Weet u zeker dat u deze gebruiker wilt uitschakelen? Deze kan dan niet meer inloggen of diensten gebruiken.",
|
||||
"ldap_soft_delete_users": "Voorkom dat in LDAP uitgeschakelde gebruikers toegang krijgen.",
|
||||
"ldap_soft_delete_users_description": "Als dit is ingeschakeld, worden gebruikers die uit LDAP worden verwijderd, uitgeschakeld in plaats van daadwerkelijk uit het systeem verwijderd.",
|
||||
"login_code_email_success": "De inlogcode is naar de gebruiker gestuurd.",
|
||||
"send_email": "Verstuur e-mail",
|
||||
"show_code": "Toon code",
|
||||
"callback_url_description": "URL's die de client heeft aangegeven. Als je dit leeg laat, worden ze automatisch toegevoegd. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je dat beter niet doen.",
|
||||
"logout_callback_url_description": "URL's die uw client heeft aangegeven om uit te loggen. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je dat beter niet doen.",
|
||||
"api_key_expiration": "API-sleutel verloopt",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Stuur een mailtje naar de gebruiker als hun API-sleutel bijna afloopt.",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Stuur een e-mail naar de gebruiker als de geldigheid van hun API-sleutel bijna verloopt.",
|
||||
"authorize_device": "Apparaat autoriseren",
|
||||
"the_device_has_been_authorized": "Het apparaat is goedgekeurd.",
|
||||
"enter_code_displayed_in_previous_step": "Voer de code in die je in de vorige stap hebt gezien.",
|
||||
"enter_code_displayed_in_previous_step": "Voer de code in die in de vorige stap werd getoond.",
|
||||
"authorize": "Autoriseren",
|
||||
"federated_client_credentials": "Federatieve clientreferenties",
|
||||
"federated_client_credentials_description": "Met federatieve clientreferenties kun je OIDC-clients verifiëren met JWT-tokens die zijn uitgegeven door andere instanties.",
|
||||
"federated_client_credentials_description": "Met federatieve clientreferenties kunt u OIDC-clients verifiëren met JWT-tokens die zijn uitgegeven door andere instanties.",
|
||||
"add_federated_client_credential": "Federatieve clientreferenties toevoegen",
|
||||
"add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe",
|
||||
"oidc_allowed_group_count": "Toegestaan aantal groepen",
|
||||
"oidc_allowed_group_count": "Aantal groepen met toegang",
|
||||
"unrestricted": "Onbeperkt",
|
||||
"show_advanced_options": "Geavanceerde opties weergeven",
|
||||
"hide_advanced_options": "Verberg geavanceerde opties",
|
||||
@@ -378,46 +380,65 @@
|
||||
"custom_accent_color": "Aangepaste accentkleur",
|
||||
"custom_accent_color_description": "Voer een eigen kleur in met een geldige CSS-kleurcode (bijvoorbeeld hex, rgb, hsl).",
|
||||
"color_value": "Kleurwaarde",
|
||||
"apply": "Solliciteren",
|
||||
"apply": "Toepassen",
|
||||
"signup_token": "Aanmeldingstoken",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "Maak een aanmeldingstoken aan om nieuwe gebruikers te laten registreren.",
|
||||
"usage_limit": "Gebruikslimiet",
|
||||
"number_of_times_token_can_be_used": "Hoe vaak je het aanmeldingstoken kunt gebruiken.",
|
||||
"number_of_times_token_can_be_used": "Hoe vaak het aanmeldingstoken gebruikt kan worden.",
|
||||
"expires": "Verloopt",
|
||||
"signup": "Aanmelden",
|
||||
"signup_requires_valid_token": "Je hebt een geldige registratietoken nodig om een account aan te maken.",
|
||||
"user_creation": "Gebruikers aanmaken",
|
||||
"configure_user_creation": "Beheer de instellingen voor het aanmaken van gebruikers, zoals hoe mensen zich kunnen aanmelden en wat nieuwe gebruikers standaard kunnen doen.",
|
||||
"user_creation_groups_description": "Wijs deze groepen automatisch toe aan nieuwe gebruikers bij aanmelding.",
|
||||
"user_creation_claims_description": "Wijs deze aangepaste claims automatisch toe aan nieuwe gebruikers bij aanmelding.",
|
||||
"user_creation_updated_successfully": "Instellingen voor het aanmaken van gebruikers zijn bijgewerkt.",
|
||||
"signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
|
||||
"signup_requires_valid_token": "U heeft een geldige registratietoken nodig om een account aan te maken.",
|
||||
"validating_signup_token": "Inlogtoken checken",
|
||||
"go_to_login": "Ga naar inloggen",
|
||||
"signup_to_appname": "Meld je aan voor {appName}",
|
||||
"create_your_account_to_get_started": "Maak je account aan om te beginnen.",
|
||||
"initial_account_creation_description": "Maak een account aan om te beginnen. Je kunt later een wachtwoord instellen.",
|
||||
"setup_your_passkey": "Stel je passkey in",
|
||||
"create_a_passkey_to_securely_access_your_account": "Maak een toegangscode aan om veilig toegang te krijgen tot je account. Dit wordt je belangrijkste manier om in te loggen.",
|
||||
"signup_to_appname": "Meld u aan voor {appName}",
|
||||
"create_your_account_to_get_started": "Om te beginnen moet u een account aanmaken.",
|
||||
"initial_account_creation_description": "Maak een account aan om te beginnen. U kunt later een wachtwoord instellen.",
|
||||
"setup_your_passkey": "Stel uw passkey in",
|
||||
"create_a_passkey_to_securely_access_your_account": "Maak een passkey aan om veilig toegang te krijgen tot je account. Dit is je primaire manier om in te loggen.",
|
||||
"skip_for_now": "Voor nu even overslaan",
|
||||
"account_created": "Account aangemaakt",
|
||||
"enable_user_signups": "Gebruikersregistratie inschakelen",
|
||||
"enable_user_signups_description": "Of de functie voor gebruikersregistratie moet worden ingeschakeld.",
|
||||
"user_signups_are_disabled": "Je kunt nu niet aanmelden.",
|
||||
"enable_user_signups_description": "Bepaal hoe mensen zich kunnen aanmelden voor nieuwe accounts in Pocket ID.",
|
||||
"user_signups_are_disabled": "Gebruikersregistraties zijn nu uitgeschakeld.",
|
||||
"create_signup_token": "Aanmeldingstoken maken",
|
||||
"view_active_signup_tokens": "Actieve aanmeldingstokens bekijken",
|
||||
"manage_signup_tokens": "Aanmeldingstokens beheren",
|
||||
"view_and_manage_active_signup_tokens": "Bekijk en beheer actieve aanmeldingstokens.",
|
||||
"signup_token_deleted_successfully": "Aanmeldingstoken succesvol verwijderd.",
|
||||
"expired": "Verlopen",
|
||||
"used_up": "Opgebruikt",
|
||||
"used_up": "Verbruikt",
|
||||
"active": "Actief",
|
||||
"usage": "Gebruik",
|
||||
"created": "Gemaakt",
|
||||
"token": "Token",
|
||||
"loading": "Bezig met laden",
|
||||
"delete_signup_token": "Registratietoken verwijderen",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Weet je zeker dat je dit aanmeldingstoken wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"signup_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Weet u zeker dat u dit aanmeldingstoken wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"signup_with_token": "Aanmelden met token",
|
||||
"signup_with_token_description": "Je kunt je alleen aanmelden met een geldige aanmeldtoken die door een beheerder is aangemaakt.",
|
||||
"signup_with_token_description": "U kunt zich alleen aanmelden met een geldige aanmeldtoken die door een beheerder is aangemaakt.",
|
||||
"signup_open": "Open inschrijving",
|
||||
"signup_open_description": "Iedereen kan zonder beperkingen een nieuw account aanmaken.",
|
||||
"of": "van",
|
||||
"skip_passkey_setup": "Pas de instellingen voor de toegangssleutel over",
|
||||
"skip_passkey_setup_description": "Het is echt een aanrader om een wachtwoord in te stellen, want zonder dat word je uit je account gegooid zodra de sessie afloopt."
|
||||
"skip_passkey_setup": "Sla de instellingen voor de toegangssleutel over",
|
||||
"skip_passkey_setup_description": "Het wordt aangeraden om een passkey in te stellen, want zonder dit kunt u niet meer inloggen zodra de sessie afloopt.",
|
||||
"my_apps": "Mijn apps",
|
||||
"no_apps_available": "Geen apps beschikbaar",
|
||||
"contact_your_administrator_for_app_access": "Neem contact op met de beheerder om toegang te krijgen tot applicaties.",
|
||||
"launch": "Openen",
|
||||
"client_launch_url": "URL voor openen door gebruiker",
|
||||
"client_launch_url_description": "De URL die wordt geopend als iemand de app start vanaf de pagina Mijn apps.",
|
||||
"client_name_description": "De naam van de client die wordt getoond in de Pocket ID UI.",
|
||||
"revoke_access": "Toegang intrekken",
|
||||
"revoke_access_description": "Toegang intrekken tot <b>{clientName}</b>. <b>{clientName}</b> kun je accountgegevens niet meer gebruiken.",
|
||||
"revoke_access_successful": "De toegang tot {clientName} is nu succesvol geblokkeerd.",
|
||||
"last_signed_in_ago": "Laatst ingelogd {time} geleden",
|
||||
"invalid_client_id": "De klant-ID mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.",
|
||||
"custom_client_id_description": "Stel een aangepaste client-ID in als je app dit nodig heeft. Anders laat je het gewoon leeg en wordt er een willekeurige ID gegenereerd.",
|
||||
"generated": "Gemaakt"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Kliknij, aby skopiować",
|
||||
"something_went_wrong": "Coś poszło nie tak",
|
||||
"go_back_to_home": "Wróć do strony głównej",
|
||||
"dont_have_access_to_your_passkey": "Nie masz dostępu do swojego klucza?",
|
||||
"alternative_sign_in_methods": "Alternatywne metody logowania",
|
||||
"login_background": "Tło logowania",
|
||||
"logo": "Logo",
|
||||
"login_code": "Kod logowania",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Klienci publiczni nie mają tajnego klucza. Są zaprojektowane dla aplikacji mobilnych, webowych i natywnych, gdzie tajne klucze nie mogą być bezpiecznie przechowywane.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Wymiana kodu publicznego klucza to funkcja zabezpieczająca, która zapobiega atakom CSRF i przechwytywaniu kodu autoryzacyjnego.",
|
||||
"requires_reauthentication": "Wymagane ponowne uwierzytelnienie",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Wymaga od użytkowników ponownego uwierzytelniania przy każdej autoryzacji, nawet jeśli są już zalogowani.",
|
||||
"name_logo": "Logo {name}",
|
||||
"change_logo": "Zmień logo",
|
||||
"upload_logo": "Prześlij logo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Liczba przypadków, w których można użyć tokenu rejestracji.",
|
||||
"expires": "Wygasają",
|
||||
"signup": "Zarejestruj się",
|
||||
"user_creation": "Tworzenie użytkowników",
|
||||
"configure_user_creation": "Zarządzaj ustawieniami tworzenia użytkowników, w tym metodami rejestracji i domyślnymi uprawnieniami dla nowych użytkowników.",
|
||||
"user_creation_groups_description": "Przypisz te grupy automatycznie nowym użytkownikom podczas rejestracji.",
|
||||
"user_creation_claims_description": "Przypisuj te niestandardowe oświadczenia automatycznie nowym użytkownikom podczas rejestracji.",
|
||||
"user_creation_updated_successfully": "Ustawienia tworzenia użytkowników zostały pomyślnie zaktualizowane.",
|
||||
"signup_disabled_description": "Rejestracja użytkowników jest całkowicie wyłączona. Tylko administratorzy mogą tworzyć nowe konta użytkowników.",
|
||||
"signup_requires_valid_token": "Aby utworzyć konto, wymagany jest ważny token rejestracyjny.",
|
||||
"validating_signup_token": "Weryfikacja tokenu rejestracji",
|
||||
"go_to_login": "Przejdź do logowania",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Pomiń na razie",
|
||||
"account_created": "Konto utworzone",
|
||||
"enable_user_signups": "Włącz rejestrację użytkowników",
|
||||
"enable_user_signups_description": "Czy funkcja rejestracji użytkowników powinna być włączona.",
|
||||
"enable_user_signups_description": "Zdecyduj, w jaki sposób użytkownicy mogą rejestrować nowe konta w Pocket ID.",
|
||||
"user_signups_are_disabled": "Rejestracja użytkowników jest obecnie wyłączona.",
|
||||
"create_signup_token": "Utwórz token rejestracji",
|
||||
"view_active_signup_tokens": "Wyświetl aktywne tokeny rejestracji",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Ładowanie",
|
||||
"delete_signup_token": "Usuń token rejestracji",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Czy na pewno chcesz usunąć ten token rejestracji? Tego działania nie można cofnąć.",
|
||||
"signup_disabled_description": "Rejestracja użytkowników jest całkowicie wyłączona. Tylko administratorzy mogą tworzyć nowe konta użytkowników.",
|
||||
"signup_with_token": "Zarejestruj się za pomocą tokenu",
|
||||
"signup_with_token_description": "Użytkownicy mogą zarejestrować się wyłącznie przy użyciu ważnego tokenu rejestracyjnego utworzonego przez administratora.",
|
||||
"signup_open": "Otwórz rejestrację",
|
||||
"signup_open_description": "Każdy może utworzyć nowe konto bez żadnych ograniczeń.",
|
||||
"of": "z",
|
||||
"skip_passkey_setup": "Pomiń konfigurację klucza dostępu",
|
||||
"skip_passkey_setup_description": "Zdecydowanie zalecamy skonfigurowanie klucza dostępu, ponieważ bez niego utracisz dostęp do konta zaraz po wygaśnięciu sesji."
|
||||
"skip_passkey_setup_description": "Zdecydowanie zalecamy skonfigurowanie klucza dostępu, ponieważ bez niego utracisz dostęp do konta zaraz po wygaśnięciu sesji.",
|
||||
"my_apps": "Moje aplikacje",
|
||||
"no_apps_available": "Brak dostępnych aplikacji",
|
||||
"contact_your_administrator_for_app_access": "Skontaktuj się z administratorem, aby uzyskać dostęp do aplikacji.",
|
||||
"launch": "Uruchomienie",
|
||||
"client_launch_url": "Adres URL uruchomienia klienta",
|
||||
"client_launch_url_description": "Adres URL, który zostanie otwarty po uruchomieniu aplikacji przez użytkownika ze strony Moje aplikacje.",
|
||||
"client_name_description": "Nazwa klienta wyświetlana w interfejsie użytkownika Pocket ID.",
|
||||
"revoke_access": "Cofnij dostęp",
|
||||
"revoke_access_description": "Cofnij dostęp do <b>{clientName}</b>. <b>{clientName}</b> nie będzie już mógł uzyskać dostępu do informacji o Twoim koncie.",
|
||||
"revoke_access_successful": "Dostęp do strony {clientName} został pomyślnie cofnięty.",
|
||||
"last_signed_in_ago": "Ostatnio zalogowany {time} temu",
|
||||
"invalid_client_id": "Identyfikator klienta może zawierać wyłącznie litery, cyfry, znaki podkreślenia i łączniki.",
|
||||
"custom_client_id_description": "Ustaw niestandardowy identyfikator klienta, jeśli jest to wymagane przez twoją aplikację. W przeciwnym razie pozostaw to pole puste, aby wygenerować losowy identyfikator.",
|
||||
"generated": "Wygenerowano"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "Clique para copiar",
|
||||
"something_went_wrong": "Algo deu errado",
|
||||
"go_back_to_home": "Voltar para o início",
|
||||
"dont_have_access_to_your_passkey": "Não tem acesso à sua chave de acesso?",
|
||||
"alternative_sign_in_methods": "Outras formas de entrar",
|
||||
"login_background": "Histórico de login",
|
||||
"logo": "Logotipo",
|
||||
"login_code": "Código de Login:",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "A troca de chaves públicas é um recurso de segurança que evita ataques CSRF e interceptação de códigos de autorização.",
|
||||
"requires_reauthentication": "Precisa autenticar de novo",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Pede que os usuários se autentiquem de novo em cada autorização, mesmo que já estejam conectados.",
|
||||
"name_logo": "{name} logotipo",
|
||||
"change_logo": "Alterar logotipo",
|
||||
"upload_logo": "Carregar logotipo",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "Número de vezes que o token de inscrição pode ser usado.",
|
||||
"expires": "Vence",
|
||||
"signup": "Cadastre-se",
|
||||
"user_creation": "Criação de usuário",
|
||||
"configure_user_creation": "Gerencie as configurações de criação de usuários, incluindo métodos de inscrição e permissões padrão para novos usuários.",
|
||||
"user_creation_groups_description": "Atribuir esses grupos automaticamente aos novos usuários quando eles se cadastrarem.",
|
||||
"user_creation_claims_description": "Atribua essas reivindicações personalizadas automaticamente aos novos usuários no momento da inscrição.",
|
||||
"user_creation_updated_successfully": "As configurações de criação do usuário foram atualizadas com sucesso.",
|
||||
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.",
|
||||
"signup_requires_valid_token": "É preciso um token de inscrição válido pra criar uma conta.",
|
||||
"validating_signup_token": "Validando o token de inscrição",
|
||||
"go_to_login": "Vá para o login",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "Pular por enquanto",
|
||||
"account_created": "Conta criada",
|
||||
"enable_user_signups": "Ativar inscrições de usuários",
|
||||
"enable_user_signups_description": "Se a funcionalidade de cadastro de usuários deve ser ativada.",
|
||||
"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.",
|
||||
"create_signup_token": "Criar token de inscrição",
|
||||
"view_active_signup_tokens": "Ver tokens de inscrição ativos",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Carregando",
|
||||
"delete_signup_token": "Apagar token de inscrição",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que quer apagar esse token de inscrição? Não dá pra voltar atrás.",
|
||||
"signup_disabled_description": "As inscrições de usuários estão totalmente desativadas. Só os administradores podem criar novas contas de usuário.",
|
||||
"signup_with_token": "Cadastre-se com token",
|
||||
"signup_with_token_description": "Os usuários só podem se cadastrar usando um token de cadastro válido criado por um administrador.",
|
||||
"signup_open": "Inscrição aberta",
|
||||
"signup_open_description": "Qualquer pessoa pode criar uma conta nova sem restrições.",
|
||||
"of": "de",
|
||||
"skip_passkey_setup": "Pular configuração da chave de acesso",
|
||||
"skip_passkey_setup_description": "É super recomendável criar uma senha de acesso, porque sem ela você vai ficar sem poder entrar na sua conta assim que a sessão acabar."
|
||||
"skip_passkey_setup_description": "É super recomendável criar uma senha de acesso, porque sem ela você vai ficar sem poder entrar na sua conta assim que a sessão acabar.",
|
||||
"my_apps": "Meus aplicativos",
|
||||
"no_apps_available": "Não tem aplicativos disponíveis",
|
||||
"contact_your_administrator_for_app_access": "Fala com o seu administrador pra conseguir acesso aos aplicativos.",
|
||||
"launch": "Lançamento",
|
||||
"client_launch_url": "URL de lançamento do cliente",
|
||||
"client_launch_url_description": "A URL que vai abrir quando alguém abrir o aplicativo na página Meus aplicativos.",
|
||||
"client_name_description": "O nome do cliente que aparece na interface do Pocket ID.",
|
||||
"revoke_access": "Revogar acesso",
|
||||
"revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não vai mais conseguir acessar as informações da sua conta.",
|
||||
"revoke_access_successful": "O acesso a {clientName} foi revogado com sucesso.",
|
||||
"last_signed_in_ago": "Último login em {time} atrás",
|
||||
"invalid_client_id": "A ID do cliente só pode ter letras, números, sublinhados e hífens.",
|
||||
"custom_client_id_description": "Defina um ID de cliente personalizado se for necessário para o seu aplicativo. Caso contrário, deixe em branco para gerar um aleatório.",
|
||||
"generated": "Gerado"
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
"click_to_copy": "Нажмите, чтобы скопировать",
|
||||
"something_went_wrong": "Что-то пошло не так",
|
||||
"go_back_to_home": "Вернуться на главную",
|
||||
"dont_have_access_to_your_passkey": "Нет доступа к вашему passkey?",
|
||||
"alternative_sign_in_methods": "Альтернативные способы входа",
|
||||
"login_background": "Фон страницы входа",
|
||||
"logo": "Логотип",
|
||||
"login_code": "Код входа",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Создайте код входа, с которым пользователь сможет войти без passkey один раз.",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Создайте код входа, с которым пользователь сможет войти без пасскея один раз.",
|
||||
"one_hour": "1 час",
|
||||
"twelve_hours": "12 часов",
|
||||
"one_day": "1 день",
|
||||
@@ -37,13 +37,13 @@
|
||||
"generate_code": "Сгенерировать код",
|
||||
"name": "Имя",
|
||||
"browser_unsupported": "Браузер не поддерживается",
|
||||
"this_browser_does_not_support_passkeys": "Этот браузер не поддерживает ключи доступа. Пожалуйста, воспользуйтесь альтернативным способом входа.",
|
||||
"this_browser_does_not_support_passkeys": "Этот браузер не поддерживает пасскеи. Пожалуйста, воспользуйтесь альтернативным способом входа.",
|
||||
"an_unknown_error_occurred": "Произошла неизвестная ошибка",
|
||||
"authentication_process_was_aborted": "Процесс аутентификации был прерван",
|
||||
"error_occurred_with_authenticator": "С аутентификатором произошла ошибка",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Аутентификатор не поддерживает discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "Аутентификатор не поддерживает resident keys",
|
||||
"passkey_was_previously_registered": "Этот passkey был ранее зарегистрирован",
|
||||
"passkey_was_previously_registered": "Этот пасскей был ранее зарегистрирован",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Аутентификатор не поддерживает ни один из запрошенных алгоритмов",
|
||||
"authenticator_timed_out": "Время ожидания аутентификатора истекло",
|
||||
"critical_error_occurred_contact_administrator": "Произошла критическая ошибка. Обратитесь к администратору.",
|
||||
@@ -62,16 +62,16 @@
|
||||
"try_again": "Попробовать снова",
|
||||
"client_logo": "Логотип клиента",
|
||||
"sign_out": "Выйти",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Вы хотите выйти из Pocket ID с учетной записью <b>{username}</b>?",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Вы хотите выйти из {appName} с учетной записью <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Вход в {appName}",
|
||||
"please_try_to_sign_in_again": "Пожалуйста, попробуйте войти снова.",
|
||||
"authenticate_with_passkey_to_access_account": "Авторизуйтесь с использованием passkey для доступа к вашей учетной записи.",
|
||||
"authenticate_with_passkey_to_access_account": "Авторизуйтесь с использованием пасскея для доступа к вашей учетной записи.",
|
||||
"authenticate": "Авторизоваться",
|
||||
"please_try_again": "Пожалуйста, повторите попытку.",
|
||||
"continue": "Продолжить",
|
||||
"alternative_sign_in": "Альтернативный вход",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Если у вас нет доступа к вашему passkey, вы можете войти одним из следующих способов.",
|
||||
"use_your_passkey_instead": "Воспользоваться passkey вместо этого?",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Если у вас нет доступа к вашему пасскею, вы можете войти одним из следующих способов.",
|
||||
"use_your_passkey_instead": "Воспользоваться пасскеем вместо этого?",
|
||||
"email_login": "Вход через электронную почту",
|
||||
"enter_a_login_code_to_sign_in": "Введите предварительно созданный код входа.",
|
||||
"request_a_login_code_via_email": "Запросить код входа на электронную почту.",
|
||||
@@ -104,15 +104,15 @@
|
||||
"account_details_updated_successfully": "Данные учетной записи успешно обновлены",
|
||||
"profile_picture_updated_successfully": "Изображение профиля успешно обновлено. Обновление может занять несколько минут.",
|
||||
"account_settings": "Настройки учетной записи",
|
||||
"passkey_missing": "Passkey отсутствует",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Пожалуйста, добавьте passkey, чтобы избежать утери доступа к вашей учетной записи.",
|
||||
"single_passkey_configured": "Настроен один passkey",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Рекомендуется добавить более одного ключа доступа во избежание потери доступа к вашей учетной записи.",
|
||||
"passkey_missing": "Пасскей отсутствует",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Пожалуйста, добавьте пасскей, чтобы избежать утери доступа к вашей учетной записи.",
|
||||
"single_passkey_configured": "Настроен один пасскей",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Рекомендуется добавить более одного пасскея во избежание потери доступа к вашей учетной записи.",
|
||||
"account_details": "Детали учетной записи",
|
||||
"passkeys": "Ключи доступа",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте ключами доступа, которые вы можете использовать для аутентификации.",
|
||||
"add_passkey": "Добавить ключ",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без passkey.",
|
||||
"passkeys": "Пасскеи",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте пасскеями, которые вы можете использовать для аутентификации себя.",
|
||||
"add_passkey": "Добавить пасскей",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без пасскея.",
|
||||
"create": "Создать",
|
||||
"first_name": "Имя",
|
||||
"last_name": "Фамилия",
|
||||
@@ -124,12 +124,12 @@
|
||||
"added_on": "Добавлен",
|
||||
"rename": "Переименовать",
|
||||
"delete": "Удалить",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Вы уверены, что хотите удалить этот passkey?",
|
||||
"passkey_deleted_successfully": "Passkey успешно удален",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Вы уверены, что хотите удалить этот пасскей?",
|
||||
"passkey_deleted_successfully": "Пасскей успешно удален",
|
||||
"delete_passkey_name": "Удалить {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Имя passkey успешно обновлено",
|
||||
"name_passkey": "Имя Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Назовите ваш passkey, чтобы легко идентифицировать его позже.",
|
||||
"passkey_name_updated_successfully": "Имя пасскея успешно обновлено",
|
||||
"name_passkey": "Имя пасскея",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Назовите ваш пасскей, чтобы легко идентифицировать его позже.",
|
||||
"create_api_key": "Создать API ключ",
|
||||
"add_a_new_api_key_for_programmatic_access": "Добавить новый API ключ для программного доступа.",
|
||||
"add_api_key": "Добавить API ключ",
|
||||
@@ -178,7 +178,7 @@
|
||||
"email_login_notification": "Уведомление о логине по электронной почте",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Отправлять пользователю письмо при входе с нового устройства.",
|
||||
"emai_login_code_requested_by_user": "Код входа по электронной почте, запрошенный пользователем",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Позволяет пользователям обходить вход через ключи доступа, запросив код входа, отправляемый на их электронную почту. Это значительно снижает безопасность, так как любой человек, имеющий доступ к электронной почте пользователя, может получить доступ.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Позволяет пользователям обходить вход через пасскей, запросив код входа, отправляемый на их электронную почту. Это значительно снижает безопасность так как любой человек, имеющий доступ к электронной почте пользователя, может получить доступ.",
|
||||
"email_login_code_from_admin": "Код входа по электронной почте от администратора",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Позволяет администратору отправлять код входа пользователю по электронной почте.",
|
||||
"send_test_email": "Отправить тестовое письмо",
|
||||
@@ -268,14 +268,16 @@
|
||||
"add_oidc_client": "Добавить OIDC клиент",
|
||||
"manage_oidc_clients": "Управление OIDC клиентами",
|
||||
"one_time_link": "Одноразовая ссылка",
|
||||
"use_this_link_to_sign_in_once": "Используйте эту ссылку, чтобы войти единожды. Это необходимо для пользователей, которые ещё не добавили passkey или потеряли его.",
|
||||
"use_this_link_to_sign_in_once": "Используйте эту ссылку, чтобы войти единожды. Это необходимо для пользователей, которые ещё не добавили пасскей или потеряли его.",
|
||||
"add": "Добавить",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Публичный клиент",
|
||||
"public_clients_description": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
|
||||
"public_clients_description": "Публичные клиенты не имеют клиентского секрета. Они предназначены для мобильных, SPA и нативных приложений, где секретные данные нельзя надежно хранить.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
|
||||
"requires_reauthentication": "Требуется повторная аутентификация",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Требует от пользователей повторной аутентификации при каждой авторизации, даже если они уже вошли в систему",
|
||||
"name_logo": "Логотип {name}",
|
||||
"change_logo": "Изменить логотип",
|
||||
"upload_logo": "Загрузить логотип",
|
||||
@@ -346,7 +348,7 @@
|
||||
"authorize_device": "Авторизовать устройство",
|
||||
"the_device_has_been_authorized": "Устройство авторизовано.",
|
||||
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
|
||||
"authorize": "Авторизируйте",
|
||||
"authorize": "Авторизовать",
|
||||
"federated_client_credentials": "Федеративные учетные данные клиента",
|
||||
"federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете авторизовывать OIDC клиентов, используя JWT токены, выпущенные третьими сторонами.",
|
||||
"add_federated_client_credential": "Добавить федеративные учетные данные клиента",
|
||||
@@ -385,18 +387,24 @@
|
||||
"number_of_times_token_can_be_used": "Количество раз, которое может быть использован токен регистрации.",
|
||||
"expires": "Истекает",
|
||||
"signup": "Зарегистрироваться",
|
||||
"user_creation": "Создание пользователя",
|
||||
"configure_user_creation": "Управляйте настройками создания пользователя, включая способы регистрации и разрешения по умолчанию для новых пользователей.",
|
||||
"user_creation_groups_description": "Назначать эти группы автоматически новым пользователям при регистрации.",
|
||||
"user_creation_claims_description": "Назначить эти пользовательские claims автоматически новым пользователям при регистрации.",
|
||||
"user_creation_updated_successfully": "Настройки создания пользователя обновлены.",
|
||||
"signup_disabled_description": "Регистрация пользователей полностью отключена. Только администраторы могут создавать новые учетные записи пользователей.",
|
||||
"signup_requires_valid_token": "Для создания учетной записи необходим действительный токен регистрации",
|
||||
"validating_signup_token": "Проверка токена регистрации",
|
||||
"go_to_login": "Перейти ко входу",
|
||||
"signup_to_appname": "Зарегистрироваться в {appName}",
|
||||
"create_your_account_to_get_started": "Создайте свою учетную запись, чтобы начать.",
|
||||
"initial_account_creation_description": "Пожалуйста, создайте свою учетную запись, чтобы начать. Вы сможете настроить passkey позже.",
|
||||
"setup_your_passkey": "Настроить passkey",
|
||||
"create_a_passkey_to_securely_access_your_account": "Создайте passkey для безопасного доступа к учетной записи. Это будет ваш основной способ входа.",
|
||||
"initial_account_creation_description": "Пожалуйста, создайте свою учетную запись, чтобы начать. Вы сможете настроить пасскей позже.",
|
||||
"setup_your_passkey": "Настройте ваш пасскей",
|
||||
"create_a_passkey_to_securely_access_your_account": "Создайте пасскей для безопасного доступа к учетной записи. Это будет ваш основной способ входа.",
|
||||
"skip_for_now": "Пока пропустить",
|
||||
"account_created": "Учетная запись создана",
|
||||
"enable_user_signups": "Включить регистрацию пользователей",
|
||||
"enable_user_signups_description": "Должна ли быть включена функция регистрации пользователя.",
|
||||
"enable_user_signups_description": "Решите, как пользователи могут регистрировать новые учетные записи в Pocket ID.",
|
||||
"user_signups_are_disabled": "Регистрация пользователей в настоящее время отключена",
|
||||
"create_signup_token": "Создать токен регистрации",
|
||||
"view_active_signup_tokens": "Показать активные токены регистрации",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "Загрузка",
|
||||
"delete_signup_token": "Удалить токен регистрации",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Вы уверены, что хотите удалить этот токен регистрации? Это действие нельзя отменить.",
|
||||
"signup_disabled_description": "Регистрация пользователей полностью отключена. Только администраторы могут создавать новые учетные записи пользователей.",
|
||||
"signup_with_token": "Регистрация с токеном",
|
||||
"signup_with_token_description": "Пользователи могут зарегистрироваться только с помощью действительного токена регистрации, созданного администратором.",
|
||||
"signup_open": "Открытая регистрация",
|
||||
"signup_open_description": "Любой может создать новую учетную запись без ограничений.",
|
||||
"of": "из",
|
||||
"skip_passkey_setup": "Пропустить настройку passkey",
|
||||
"skip_passkey_setup_description": "Настоятельно рекомендуется настроить passkey, так как без него вы более не сможете войти в учетную запись после истечения сессии."
|
||||
"skip_passkey_setup": "Пропустить настройку пасскея",
|
||||
"skip_passkey_setup_description": "Настоятельно рекомендуется настроить пасскей, так как без него вы более не сможете войти в учетную запись после истечения сессии.",
|
||||
"my_apps": "Мои приложения",
|
||||
"no_apps_available": "Нет доступных приложений",
|
||||
"contact_your_administrator_for_app_access": "Свяжись с администратором, чтобы получить доступ к приложениям.",
|
||||
"launch": "Запустить",
|
||||
"client_launch_url": "Клиентский URL для запуска",
|
||||
"client_launch_url_description": "URL-адрес, который откроется, когда кто-то запустит приложение со страницы «Мои приложения».",
|
||||
"client_name_description": "Имя клиента, которое отображается в интерфейсе Pocket ID.",
|
||||
"revoke_access": "Отозвать доступ",
|
||||
"revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.",
|
||||
"revoke_access_successful": "Доступ к {clientName} успешно отозван.",
|
||||
"last_signed_in_ago": "Последний вход {time} назад",
|
||||
"invalid_client_id": "ID клиента может содержать только буквы, цифры, подчеркивания и дефисы",
|
||||
"custom_client_id_description": "Установите пользовательский ID клиента, если это нужно для вашего приложения. Если нет, оставьте поле пустым, чтобы он был сгенерирован случайным образом.",
|
||||
"generated": "Сгенерированный"
|
||||
}
|
||||
|
||||
444
frontend/messages/uk.json
Normal file
444
frontend/messages/uk.json
Normal file
@@ -0,0 +1,444 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Мій обліковий запис",
|
||||
"logout": "Вийти",
|
||||
"confirm": "Підтвердити",
|
||||
"docs": "Документація",
|
||||
"key": "Ключ",
|
||||
"value": "Значення",
|
||||
"remove_custom_claim": "Видалити власний атрибут",
|
||||
"add_custom_claim": "Додати власний атрибут",
|
||||
"add_another": "Додати ще",
|
||||
"select_a_date": "Обрати дату",
|
||||
"select_file": "Обрати файл",
|
||||
"profile_picture": "Фотографія профілю",
|
||||
"profile_picture_is_managed_by_ldap_server": "Фотографія профілю управляється сервером LDAP і не може бути змінена тут.",
|
||||
"click_profile_picture_to_upload_custom": "Натисніть на зображення профілю, щоб завантажити власне зображення.",
|
||||
"image_should_be_in_format": "Зображення повинно бути у форматі PNG або JPEG.",
|
||||
"items_per_page": "Елементів на сторінці",
|
||||
"no_items_found": "Нічого не знайдено",
|
||||
"search": "Пошук...",
|
||||
"expand_card": "Розгорнути картку",
|
||||
"copied": "Скопійовано",
|
||||
"click_to_copy": "Натисніть, щоб скопіювати",
|
||||
"something_went_wrong": "Щось пішло не так",
|
||||
"go_back_to_home": "Повернутися на головну сторінку",
|
||||
"alternative_sign_in_methods": "Альтернативні способи входу",
|
||||
"login_background": "Фон сторінки входу",
|
||||
"logo": "Логотип",
|
||||
"login_code": "Код входу",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Створіть код входу, який користувач може використовувати для входу без ключа доступу одноразово.",
|
||||
"one_hour": "1 година",
|
||||
"twelve_hours": "12 годин",
|
||||
"one_day": "1 день",
|
||||
"one_week": "1 тиждень",
|
||||
"one_month": "1 місяць",
|
||||
"expiration": "Термін дії",
|
||||
"generate_code": "Генерувати код",
|
||||
"name": "Назва",
|
||||
"browser_unsupported": "Браузер не підтримується",
|
||||
"this_browser_does_not_support_passkeys": "Цей браузер не підтримує ключі доступу. Будь ласка, скористайтеся іншим способом входу.",
|
||||
"an_unknown_error_occurred": "Сталася невідома помилка",
|
||||
"authentication_process_was_aborted": "Процес автентифікації було перервано",
|
||||
"error_occurred_with_authenticator": "Сталася помилка з автентифікатором",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Автентифікатор не підтримує виявлені облікові дані",
|
||||
"authenticator_does_not_support_resident_keys": "Автентифікатор не підтримує локальні ключі",
|
||||
"passkey_was_previously_registered": "Цей ключ доступу був раніше зареєстрований",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Автентифікатор не підтримує жоден із запитаних алгоритмів",
|
||||
"authenticator_timed_out": "Час очікування автентифікатора вичерпано",
|
||||
"critical_error_occurred_contact_administrator": "Виникла критична помилка. Будь ласка, зверніться до адміністратора.",
|
||||
"sign_in_to": "Увійти в {name}",
|
||||
"client_not_found": "Клієнта не знайдено",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> хоче отримати доступ до наступної інформації:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Бажаєте увійти до <b>{client}</b> за допомогою облікового запису {appName}?",
|
||||
"email": "Електронна пошта",
|
||||
"view_your_email_address": "Переглянути адресу електронної пошти",
|
||||
"profile": "Профіль",
|
||||
"view_your_profile_information": "Переглянути інформацію про свій профіль",
|
||||
"groups": "Групи",
|
||||
"view_the_groups_you_are_a_member_of": "Переглянути групи, учасником яких ви є",
|
||||
"cancel": "Скасувати",
|
||||
"sign_in": "Увійти",
|
||||
"try_again": "Спробувати знову",
|
||||
"client_logo": "Логотип клієнта",
|
||||
"sign_out": "Вийти",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Ви хочете вийти з {appName} під обліковим записом <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Увійти в {appName}",
|
||||
"please_try_to_sign_in_again": "Будь ласка, спробуйте увійти знову.",
|
||||
"authenticate_with_passkey_to_access_account": "Автентифікуйтеся за допомогою ключа доступу, щоб отримати доступ до свого облікового запису.",
|
||||
"authenticate": "Автентифікуватися",
|
||||
"please_try_again": "Будь ласка, спробуйте ще раз.",
|
||||
"continue": "Продовжити",
|
||||
"alternative_sign_in": "Альтернативний вхід",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Якщо у вас немає доступу до ключа доступу, ви можете увійти, використавши один із наступних способів.",
|
||||
"use_your_passkey_instead": "Використати ключ доступу натомість?",
|
||||
"email_login": "Вхід за електронною поштою",
|
||||
"enter_a_login_code_to_sign_in": "Введіть код для входу, щоб увійти.",
|
||||
"request_a_login_code_via_email": "Запросити код для входу електронною поштою.",
|
||||
"go_back": "Назад",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Електронний лист було надіслано на вказану електронну адресу, якщо вона існує в системі.",
|
||||
"enter_code": "Введіть код",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Введіть вашу електронну адресу, щоб отримати лист із кодом для входу.",
|
||||
"your_email": "Ваша електронна адреса",
|
||||
"submit": "Надіслати",
|
||||
"enter_the_code_you_received_to_sign_in": "Введіть отриманий код, щоб увійти.",
|
||||
"code": "Код",
|
||||
"invalid_redirect_url": "Неправильна URL-адреса перенаправлення",
|
||||
"audit_log": "Журнал авдиту",
|
||||
"users": "Користувачі",
|
||||
"user_groups": "Групи користувачів",
|
||||
"oidc_clients": "Клієнти OIDC",
|
||||
"api_keys": "Ключі API",
|
||||
"application_configuration": "Конфігурація застосунку",
|
||||
"settings": "Налаштування",
|
||||
"update_pocket_id": "Оновити Pocket ID",
|
||||
"powered_by": "Працює на базі",
|
||||
"see_your_account_activities_from_the_last_3_months": "Перегляньте активність вашого облікового запису за останні 3 місяці.",
|
||||
"time": "Час",
|
||||
"event": "Подія",
|
||||
"approximate_location": "Приблизне місцеперебування",
|
||||
"ip_address": "IP-адреса",
|
||||
"device": "Пристрій",
|
||||
"client": "Клієнт",
|
||||
"unknown": "Невідомий",
|
||||
"account_details_updated_successfully": "Дані облікового запису успішно оновлені",
|
||||
"profile_picture_updated_successfully": "Зображення профілю успішно оновлено. Оновлення може зайняти кілька хвилин.",
|
||||
"account_settings": "Налаштування облікового запису",
|
||||
"passkey_missing": "Ключ доступу відсутній",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Будь ласка, додайте ключ доступу для запобігання втрати доступу до вашого облікового запису.",
|
||||
"single_passkey_configured": "Налаштовано єдиний ключ доступу",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Рекомендується додати більше одного ключа доступу, щоб уникнути втрати доступу до вашого облікового запису.",
|
||||
"account_details": "Дані облікового запису",
|
||||
"passkeys": "Ключі доступу",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Керуйте своїми ключами доступу, які ви можете використовувати для автентифікації.",
|
||||
"add_passkey": "Додати ключ доступу",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Створіть одноразовий код для входу, щоб увійти з іншого пристрою без ключа доступу.",
|
||||
"create": "Створити",
|
||||
"first_name": "Ім'я",
|
||||
"last_name": "Прізвище",
|
||||
"username": "Ім’я користувача",
|
||||
"save": "Зберегти",
|
||||
"username_can_only_contain": "Ім’я користувача може містити лише малі літери, цифри, підкреслення, крапки, дефіси та символ '@'",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Увійдіть, використовуючи наступний код. Код дійсний протягом 15 хвилин.",
|
||||
"or_visit": "або відвідайте",
|
||||
"added_on": "Додано",
|
||||
"rename": "Перейменувати",
|
||||
"delete": "Видалити",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Ви впевнені, що хочете видалити цей ключ доступу?",
|
||||
"passkey_deleted_successfully": "Ключ доступу успішно видалено",
|
||||
"delete_passkey_name": "Видалити {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Назва ключа доступу успішно оновлено",
|
||||
"name_passkey": "Назва ключа доступу",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Назвіть свій ключ доступу, щоб легко пізнати його пізніше.",
|
||||
"create_api_key": "Створити API-ключ",
|
||||
"add_a_new_api_key_for_programmatic_access": "Додайте новий API-ключ для програмного доступу.",
|
||||
"add_api_key": "Додати API-ключ",
|
||||
"manage_api_keys": "Керувати ключами API",
|
||||
"api_key_created": "Створено API-ключ",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "З міркувань безпеки цей ключ буде показано лише один раз. Будь ласка, збережіть його в безпечному місці.",
|
||||
"description": "Опис",
|
||||
"api_key": "API-ключ",
|
||||
"close": "Закрити",
|
||||
"name_to_identify_this_api_key": "Назва для ідентифікації цього API-ключа.",
|
||||
"expires_at": "Дійсно до",
|
||||
"when_this_api_key_will_expire": "Коли спливе термін дії цього API-ключа.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Додатковий опис для допомоги в ідентифікації призначення цього ключа (необов’язково).",
|
||||
"expiration_date_must_be_in_the_future": "Дата закінчення терміну дії повинна бути в майбутньому",
|
||||
"revoke_api_key": "Анулювати API-ключ",
|
||||
"never": "Ніколи",
|
||||
"revoke": "Анулювати",
|
||||
"api_key_revoked_successfully": "API-ключ успішно анульовано",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Ви впевнені, що хочете анулювати API-ключ «{apiKeyName}»? Це призведе до зупинки всіх інтеграцій, які використовують цей ключ.",
|
||||
"last_used": "Останнє використання",
|
||||
"actions": "Дії",
|
||||
"images_updated_successfully": "Зображення успішно оновлено",
|
||||
"general": "Загальне",
|
||||
"configure_smtp_to_send_emails": "Увімкніть сповіщення електронною поштою, щоб повідомляти користувачів про вхід з нового пристрою або місцеперебування.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Налаштуйте параметри LDAP для синхронізації користувачів і груп із LDAP-сервера.",
|
||||
"images": "Зображення",
|
||||
"update": "Оновити",
|
||||
"email_configuration_updated_successfully": "Конфігурацію електронної пошти успішно оновлено",
|
||||
"save_changes_question": "Зберегти зміни?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Ви повинні зберегти зміни перед надсиланням тестового листа. Зберегти зараз?",
|
||||
"save_and_send": "Зберегти та надіслати",
|
||||
"test_email_sent_successfully": "Тестовий лист успішно відправлено на вашу електронну адресу.",
|
||||
"failed_to_send_test_email": "Не вдалося надіслати тестовий лист. Перевірте журнали сервера для отримання додаткової інформації.",
|
||||
"smtp_configuration": "Налаштування SMTP",
|
||||
"smtp_host": "SMTP хост",
|
||||
"smtp_port": "SMTP порт",
|
||||
"smtp_user": "SMTP користувач",
|
||||
"smtp_password": "SMTP пароль",
|
||||
"smtp_from": "Відправник",
|
||||
"smtp_tls_option": "Тип SMTP TLS",
|
||||
"email_tls_option": "TLS налаштування електронної пошти",
|
||||
"skip_certificate_verification": "Пропустити перевірку сертифіката",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Ця опція може бути корисною для самопідписаних сертифікатів.",
|
||||
"enabled_emails": "Увімкнені електронні листи",
|
||||
"email_login_notification": "Сповіщення електронною поштою про вхід",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Надіслати електронний лист користувачеві після входу з нового пристрою.",
|
||||
"emai_login_code_requested_by_user": "Надіслати коду входу, згенерований користувачем, електронною поштою",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Дозволяє користувачам обходити ключі доступу шляхом запиту коду для входу, який був відправлений на їх електронну пошту. Це суттєво зменшує безпеку, оскільки будь-хто, хто має доступ до електронної пошти користувача, може отримати доступ.",
|
||||
"email_login_code_from_admin": "Надіслати коду входу, згенерований адміністратором, електронною поштою",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Дозволяє адміністратору надсилати код для входу користувачеві електронною поштою.",
|
||||
"send_test_email": "Відправити тестового листа",
|
||||
"application_configuration_updated_successfully": "Налаштування додатку успішно оновлено",
|
||||
"application_name": "Назва додатку",
|
||||
"session_duration": "Тривалість сеансу",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Тривалість сесії у хвилинах до повторного входу користувача.",
|
||||
"enable_self_account_editing": "Увімкнути редагування власного облікового запису",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Чи повинні користувачі мати можливість редагувати власні дані облікового запису.",
|
||||
"emails_verified": "Підтверджена електронна пошта",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Чи слід позначати електронну пошту користувача як підтверджену для OIDC клієнтів.",
|
||||
"ldap_configuration_updated_successfully": "Налаштування LDAP успішно оновлено",
|
||||
"ldap_disabled_successfully": "LDAP успішно вимкнено",
|
||||
"ldap_sync_finished": "Синхронізація LDAP завершена",
|
||||
"client_configuration": "Налаштування клієнтів",
|
||||
"ldap_url": "URL-адреса LDAP",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "Пароль LDAP Bind",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "Фільтр пошуку користувачів",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Фільтр пошуку для пошуку/синхронізації користувачів.",
|
||||
"groups_search_filter": "Фільтр пошуку груп",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Фільтр пошуку для пошуку/синхронізації груп.",
|
||||
"attribute_mapping": "Зіставлення атрибутів",
|
||||
"user_unique_identifier_attribute": "Атрибут для унікального ідентифікатора користувача",
|
||||
"the_value_of_this_attribute_should_never_change": "Значення цього атрибуту ніколи не повинно змінюватися.",
|
||||
"username_attribute": "Атрибут «Ім’я користувача»",
|
||||
"user_mail_attribute": "Атрибут \"Електронна пошта\" користувача",
|
||||
"user_first_name_attribute": "Атрибут \"Ім'я\" користувача",
|
||||
"user_last_name_attribute": "Атрибут \"Прізвище\" користувача",
|
||||
"user_profile_picture_attribute": "Атрибут \"Зображення профілю\" користувача",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Значення цього атрибута може бути URL, бінарним файлом або зображенням, закодованим у форматі base64.",
|
||||
"group_members_attribute": "Атрибут \"Учасник груп\"",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Атрибут, який використовується для запиту учасників групи.",
|
||||
"group_unique_identifier_attribute": "Атрибут \"Унікальний ідентифікатор групи\"",
|
||||
"group_name_attribute": "Атрибут \"Назва групи\"",
|
||||
"admin_group_name": "Атрибут \"Назва групи адміністратора\"",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Учасники цієї групи матимуть адміністративні права в Pocket ID.",
|
||||
"disable": "Вимкнути",
|
||||
"sync_now": "Синхронізувати",
|
||||
"enable": "Увімкнути",
|
||||
"user_created_successfully": "Користувача успішно створено",
|
||||
"create_user": "Створити користувача",
|
||||
"add_a_new_user_to_appname": "Додати нового користувача до {appName}",
|
||||
"add_user": "Додати користувача",
|
||||
"manage_users": "Керування користувачами",
|
||||
"admin_privileges": "Права адміністратора",
|
||||
"admins_have_full_access_to_the_admin_panel": "Адміністратори мають повний доступ до панелі адміністратора.",
|
||||
"delete_firstname_lastname": "Видалити {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Ви впевнені, що хочете видалити цього користувача?",
|
||||
"user_deleted_successfully": "Користувача успішно видалено",
|
||||
"role": "Роль",
|
||||
"source": "Джерело",
|
||||
"admin": "Адміністратор",
|
||||
"user": "Користувач",
|
||||
"local": "Локальний",
|
||||
"toggle_menu": "Відкрити меню",
|
||||
"edit": "Редагувати",
|
||||
"user_groups_updated_successfully": "Групи користувачів успішно оновлено",
|
||||
"user_updated_successfully": "Користувача успішно оновлено",
|
||||
"custom_claims_updated_successfully": "Власні атрибути успішно оновлено",
|
||||
"back": "Назад",
|
||||
"user_details_firstname_lastname": "Дані користувача {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Керування групами, до яких належить цей користувач.",
|
||||
"custom_claims": "Власні атрибути",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Власні атрибути — це пари ключ-значення, які використовуються для зберігання додаткової інформації про користувача. Ці атрибути будуть включені до ID-токена при запиті області 'profile'.",
|
||||
"user_group_created_successfully": "Групи користувачів успішно створено",
|
||||
"create_user_group": "Створити групу користувачів",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Створіть нову групу, яку можна призначити користувачам.",
|
||||
"add_group": "Створити групу",
|
||||
"manage_user_groups": "Керування групами користувачів",
|
||||
"friendly_name": "Зручна назва",
|
||||
"name_that_will_be_displayed_in_the_ui": "Назва, яка буде показуватися в інтерфейсі користувача",
|
||||
"name_that_will_be_in_the_groups_claim": "Назва, яка буде в атрибуті \"groups\"",
|
||||
"delete_name": "Видалити {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Ви впевнені, що хочете видалити цю групу користувачів?",
|
||||
"user_group_deleted_successfully": "Групу користувачів успішно видалено",
|
||||
"user_count": "Кількість користувачів",
|
||||
"user_group_updated_successfully": "Групу користувачів успішно оновлено",
|
||||
"users_updated_successfully": "Користувачі успішно оновлені",
|
||||
"user_group_details_name": "Деталі групи користувачів {name}",
|
||||
"assign_users_to_this_group": "Призначити користувачів до цієї групи.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Власні атрибути — це пари ключ-значення, які використовуються для зберігання додаткової інформації про користувача. Ці атрибути будуть включені до ID-токена при запиті області 'profile'. Якщо виникають конфлікти, пріоритет мають власні атрибути, визначені для конкретного користувача.",
|
||||
"oidc_client_created_successfully": "OIDC-клієнт успішно створений",
|
||||
"create_oidc_client": "Створити OIDC-клієнт",
|
||||
"add_a_new_oidc_client_to_appname": "Додати новий OIDC-клієнт до {appName}.",
|
||||
"add_oidc_client": "Додати OIDC-клієнт",
|
||||
"manage_oidc_clients": "Керувати OIDC-клієнтами",
|
||||
"one_time_link": "Одноразове посилання",
|
||||
"use_this_link_to_sign_in_once": "Використайте це посилання для одноразового входу. Це потрібно для користувачів, які ще не додали ключ доступу або втратили його.",
|
||||
"add": "Додати",
|
||||
"callback_urls": "URL-адреси зворотного виклику",
|
||||
"logout_callback_urls": "URL-адреси зворотного виклику для виходу",
|
||||
"public_client": "Публічний клієнт",
|
||||
"public_clients_description": "Публічні клієнти не мають секретного ключа. Вони призначені для мобільних, веб та нативних додатків, де секретний ключ не може надійно зберігатись.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — це функція безпеки, що запобігає атакам типу CSRF та перехопленню коду авторизації.",
|
||||
"requires_reauthentication": "Потрібна повторна автентифікація",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Вимагає від користувачів повторної автентифікації при кожній авторизації, навіть якщо вони вже ввійшли в систему.",
|
||||
"name_logo": "Логотип {name}",
|
||||
"change_logo": "Змінити логотип",
|
||||
"upload_logo": "Вивантажити логотип",
|
||||
"remove_logo": "Видалити логотип",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Ви дійсно хочете видалити цей OIDC-клієнт?",
|
||||
"oidc_client_deleted_successfully": "OIDC-клієнт успішно видалений",
|
||||
"authorization_url": "URL-адреса Authorization",
|
||||
"oidc_discovery_url": "URL-адреса ODC Discovery",
|
||||
"token_url": "URL-адреса Token",
|
||||
"userinfo_url": "URL-адреса Userinfo",
|
||||
"logout_url": "URL-адреса Logout",
|
||||
"certificate_url": "URL-адреса Certificate",
|
||||
"enabled": "Увімкнено",
|
||||
"disabled": "Вимкнено",
|
||||
"oidc_client_updated_successfully": "OIDC-клієнт успішно оновлений",
|
||||
"create_new_client_secret": "Створити новий секретний ключ клієнта",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Ви впевнені, що хочете створити новий секретний ключ клієнта? Старий стане недійсним.",
|
||||
"generate": "Згенерувати",
|
||||
"new_client_secret_created_successfully": "Новий секретний ключ клієнта успішно створено",
|
||||
"allowed_user_groups_updated_successfully": "Дозволені групи користувачів успішно оновлено",
|
||||
"oidc_client_name": "OIDC-клієнт {name}",
|
||||
"client_id": "ID клієнта",
|
||||
"client_secret": "Секретний ключ клієнта",
|
||||
"show_more_details": "Показати подробиці",
|
||||
"allowed_user_groups": "Дозволені групи користувачів",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Додайте групи користувачів до цього клієнта, щоб обмежити доступ користувачам лише з цих груп. Якщо жодна група користувачів не обрана, усі користувачі матимуть доступ до цього клієнта.",
|
||||
"favicon": "Фавікон",
|
||||
"light_mode_logo": "Логотип світлого режиму",
|
||||
"dark_mode_logo": "Логотип темного режиму",
|
||||
"background_image": "Фонове зображення",
|
||||
"language": "Мова",
|
||||
"reset_profile_picture_question": "Скинути зображення профілю?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Це видалить завантажене зображення та скине фото профілю на стандартне. Продовжити?",
|
||||
"reset": "Скинути",
|
||||
"reset_to_default": "Відновити налаштування за замовчуванням",
|
||||
"profile_picture_has_been_reset": "Фотографію профілю скинуто. Оновлення може зайняти кілька хвилин.",
|
||||
"select_the_language_you_want_to_use": "Виберіть мову, яку бажаєте використовувати. Зверніть увагу, що деякий текст може бути автоматично перекладений і може містити неточності.",
|
||||
"contribute_to_translation": "Якщо ви знайдете помилку, ви можете долучитися до перекладу на <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Особисте",
|
||||
"global": "Загальні",
|
||||
"all_users": "Усі користувачі",
|
||||
"all_events": "Усі події",
|
||||
"all_clients": "Усі клієнти",
|
||||
"all_locations": "Усі місця розташування",
|
||||
"global_audit_log": "Глобальний журнал авдиту",
|
||||
"see_all_account_activities_from_the_last_3_months": "Переглянути всю активність користувача за останні 3 місяці.",
|
||||
"token_sign_in": "Вхід за допомогою токена",
|
||||
"client_authorization": "Авторизація клієнта",
|
||||
"new_client_authorization": "Нова авторизація клієнта",
|
||||
"disable_animations": "Вимкнути анімацію",
|
||||
"turn_off_ui_animations": "Вимкнути анімації у всьому інтерфейсі.",
|
||||
"user_disabled": "Обліковий запис вимкнено",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Вимкнені користувачі не можуть увійти в систему або користуватися послугами.",
|
||||
"user_disabled_successfully": "Користувача успішно деактивовано.",
|
||||
"user_enabled_successfully": "Користувача успішно активовано.",
|
||||
"status": "Статус",
|
||||
"disable_firstname_lastname": "Деактивувати {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Ви впевнені, що хочете вимкнути цього користувача? Він не зможе увійти в систему або користуватися будь-якими послугами.",
|
||||
"ldap_soft_delete_users": "Зберігати вимкнених користувачів із LDAP.",
|
||||
"ldap_soft_delete_users_description": "Якщо увімкнено, користувачів, видалених з LDAP, буде вимкнено, а не видалено з системи.",
|
||||
"login_code_email_success": "Код для входу було надіслано користувачеві.",
|
||||
"send_email": "Надіслати електронного листа",
|
||||
"show_code": "Показати код",
|
||||
"callback_url_description": "URL-адреси надані вашим клієнтом. Якщо поле залишити порожнім, вони будуть додані автоматично. Підтримуються символи-замінники (*), але краще їх уникати для підвищення безпеки.",
|
||||
"logout_callback_url_description": "URL-адреси надані вашим клієнтом для виходу з системи. Якщо поле залишити порожнім, вони будуть додані автоматично. Підтримуються символи-замінники (*), але краще їх уникати для підвищення безпеки.",
|
||||
"api_key_expiration": "Термін дії API-ключа",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Надіслати користувачу електронного листа, коли термін дії його API-ключа наближається до завершення.",
|
||||
"authorize_device": "Авторизувати пристрій",
|
||||
"the_device_has_been_authorized": "Пристрій авторизовано.",
|
||||
"enter_code_displayed_in_previous_step": "Введіть код, який було показано на попередньому кроці.",
|
||||
"authorize": "Авторизувати",
|
||||
"federated_client_credentials": "Федеративні облікові дані клієнта",
|
||||
"federated_client_credentials_description": "За допомогою федеративних облікових даних клієнта ви можете автентифікувати клієнтів OIDC за допомогою токенів JWT, виданих третіми сторонами.",
|
||||
"add_federated_client_credential": "Додати федеративний обліковий запис клієнта",
|
||||
"add_another_federated_client_credential": "Додати ще один федеративний обліковий запис клієнта",
|
||||
"oidc_allowed_group_count": "Кількість дозволених груп",
|
||||
"unrestricted": "Не обмежено",
|
||||
"show_advanced_options": "Показати розширені параметри",
|
||||
"hide_advanced_options": "Приховати розширені параметри",
|
||||
"oidc_data_preview": "Попередній перегляд даних OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Переглянути дані OIDC, які будуть надіслані для різних користувачів",
|
||||
"id_token": "ID-токен",
|
||||
"access_token": "Токен доступу",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "Вміст ID-токена",
|
||||
"access_token_payload": "Вміст токена доступу",
|
||||
"userinfo_endpoint_response": "Відповідь сервісу Userinfo",
|
||||
"copy": "Копіювати",
|
||||
"no_preview_data_available": "Попередній перегляд даних недоступний",
|
||||
"copy_all": "Скопіювати все",
|
||||
"preview": "Попередній перегляд",
|
||||
"preview_for_user": "Попередній перегляд для {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Попередній перегляд OIDC-даних для цього користувача",
|
||||
"show": "Показати",
|
||||
"select_an_option": "Обрати варіант",
|
||||
"select_user": "Обрати користувача",
|
||||
"error": "Помилка",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Оберіть колір підсвічування, щоб налаштувати зовнішній вигляд Pocket ID.",
|
||||
"accent_color": "Колір підсвічування",
|
||||
"custom_accent_color": "Власний колір підсвічування",
|
||||
"custom_accent_color_description": "Введіть власний колір, використовуючи допустимі формати кольорів CSS (наприклад, hex, rgb, hsl).",
|
||||
"color_value": "Значення кольору",
|
||||
"apply": "Застосувати",
|
||||
"signup_token": "Токен реєстрації",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "Створити токен реєстрації, щоб дозволити реєстрацію нових користувачів.",
|
||||
"usage_limit": "Створити токен для реєстрації нового користувача",
|
||||
"number_of_times_token_can_be_used": "Скільки разів можна використовувати реєстраційний токен.",
|
||||
"expires": "Термін дії",
|
||||
"signup": "Зареєструватися",
|
||||
"user_creation": "Створення користувача",
|
||||
"configure_user_creation": "Керуйте налаштуваннями створення користувачів, включаючи методи реєстрації та права доступу за замовчуванням для нових користувачів.",
|
||||
"user_creation_groups_description": "Призначте ці групи автоматично новим користувачам під час реєстрації.",
|
||||
"user_creation_claims_description": "Призначте ці власні вимоги автоматично новим користувачам під час реєстрації.",
|
||||
"user_creation_updated_successfully": "Налаштування створення користувача успішно оновлено.",
|
||||
"signup_disabled_description": "Реєстрація користувачів повністю вимкнена. Нові облікові записи можуть створювати лише адміністратори.",
|
||||
"signup_requires_valid_token": "Для створення облікового запису потрібен дійсний токен реєстрації",
|
||||
"validating_signup_token": "Перевірка токена реєстрації",
|
||||
"go_to_login": "Перейти до входу",
|
||||
"signup_to_appname": "Зареєструватися в {appName}",
|
||||
"create_your_account_to_get_started": "Створіть свій обліковий запис, щоб розпочати.",
|
||||
"initial_account_creation_description": "Будь ласка, створіть свій обліковий запис, щоб почати. Ви зможете налаштувати ключ доступу пізніше.",
|
||||
"setup_your_passkey": "Налаштуйте свій ключ доступу",
|
||||
"create_a_passkey_to_securely_access_your_account": "Створіть ключ доступу для безпечного входу до свого облікового запису. Це буде ваш основний спосіб увійти.",
|
||||
"skip_for_now": "Пропустити наразі",
|
||||
"account_created": "Обліковий запис створено",
|
||||
"enable_user_signups": "Дозволити реєстрацію користувачів",
|
||||
"enable_user_signups_description": "Визначте, як користувачі можуть реєструвати нові облікові записи в Pocket ID.",
|
||||
"user_signups_are_disabled": "Реєстрація користувачів наразі вимкнена",
|
||||
"create_signup_token": "Створити токен реєстрації",
|
||||
"view_active_signup_tokens": "Переглянути активні токени реєстрації",
|
||||
"manage_signup_tokens": "Керування токенами реєстрації",
|
||||
"view_and_manage_active_signup_tokens": "Перегляд та керування активними токенами реєстрації.",
|
||||
"signup_token_deleted_successfully": "Токен реєстрації успішно видалено.",
|
||||
"expired": "Закінчився",
|
||||
"used_up": "Використаний",
|
||||
"active": "Активний",
|
||||
"usage": "Використано",
|
||||
"created": "Створено",
|
||||
"token": "Токен",
|
||||
"loading": "Завантаження",
|
||||
"delete_signup_token": "Видалити токен реєстрації",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Ви впевнені, що хочете видалити цей токен реєстрації? Цю дію неможливо скасувати.",
|
||||
"signup_with_token": "Зареєструватися за допомогою токену",
|
||||
"signup_with_token_description": "Користувачі можуть зареєструватися лише за допомогою дійсного токена реєстрації, створеного адміністратором.",
|
||||
"signup_open": "Відкрита реєстрація",
|
||||
"signup_open_description": "Будь-хто може створити новий обліковий запис без обмежень.",
|
||||
"of": "з",
|
||||
"skip_passkey_setup": "Пропустити налаштування ключа доступу",
|
||||
"skip_passkey_setup_description": "Рекомендується налаштувати ключ доступу, оскільки без нього ви не зможете увійти у свій обліковий запис після закінчення сеансу.",
|
||||
"my_apps": "Мої додатки",
|
||||
"no_apps_available": "Немає доступних додатків",
|
||||
"contact_your_administrator_for_app_access": "Зверніться до адміністратора, щоб отримати доступ до додатків.",
|
||||
"launch": "Запуск",
|
||||
"client_launch_url": "URL-адреса для запуску клієнта",
|
||||
"client_launch_url_description": "URL-адреса, яка відкриється, коли користувач запустить програму зі сторінки «Мої програми».",
|
||||
"client_name_description": "Назва клієнта, яке відображається в інтерфейсі Pocket ID.",
|
||||
"revoke_access": "Скасувати доступ",
|
||||
"revoke_access_description": "Скасувати доступ для <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.",
|
||||
"revoke_access_successful": "Доступ для {clientName} було успішно скасовано.",
|
||||
"last_signed_in_ago": "Останній вхід {time} тому",
|
||||
"invalid_client_id": "Ідентифікатор клієнта може містити тільки літери, цифри, підкреслення та дефіси.",
|
||||
"custom_client_id_description": "Встановіть власний ідентифікатор клієнта, якщо це потрібно для вашої програми. В іншому випадку залиште поле порожнім, щоб створити випадковий ідентифікатор.",
|
||||
"generated": "Створено"
|
||||
}
|
||||
444
frontend/messages/vi.json
Normal file
444
frontend/messages/vi.json
Normal file
@@ -0,0 +1,444 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Tài khoản",
|
||||
"logout": "Đăng xuất",
|
||||
"confirm": "Xác nhận",
|
||||
"docs": "Tài liệu",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Xóa yêu cầu tùy chỉnh",
|
||||
"add_custom_claim": "Thêm yêu cầu tùy chỉnh",
|
||||
"add_another": "Thêm khác",
|
||||
"select_a_date": "Chọn ngày",
|
||||
"select_file": "Chọn tập tin",
|
||||
"profile_picture": "Ảnh đại diện",
|
||||
"profile_picture_is_managed_by_ldap_server": "Hình đại diện được quản lý bởi máy chủ LDAP và không thể thay đổi tại đây.",
|
||||
"click_profile_picture_to_upload_custom": "Nhấp vào hình ảnh hồ sơ để tải lên hình ảnh tùy chỉnh.",
|
||||
"image_should_be_in_format": "Hình ảnh phải ở định dạng PNG hoặc JPEG.",
|
||||
"items_per_page": "Số kết quả mỗi trang",
|
||||
"no_items_found": "Không tìm thấy kết quả nào",
|
||||
"search": "Tìm kiếm...",
|
||||
"expand_card": "Mở rộng thẻ",
|
||||
"copied": "Đã sao chép",
|
||||
"click_to_copy": "Nhấn để sao chép",
|
||||
"something_went_wrong": "Đã xảy ra lỗi",
|
||||
"go_back_to_home": "Quay lại trang chủ",
|
||||
"alternative_sign_in_methods": "Các phương thức đăng nhập thay thế",
|
||||
"login_background": "Hình nền đăng nhập",
|
||||
"logo": "Logo",
|
||||
"login_code": "Mã đăng nhập",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Tạo một mã đăng nhập mà người dùng có thể sử dụng để đăng nhập mà không cần passkey (một lần).",
|
||||
"one_hour": "1 giờ",
|
||||
"twelve_hours": "12 giờ",
|
||||
"one_day": "1 ngày",
|
||||
"one_week": "1 tuần",
|
||||
"one_month": "1 tháng",
|
||||
"expiration": "Hết hiệu lực",
|
||||
"generate_code": "Tạo mã",
|
||||
"name": "Tên",
|
||||
"browser_unsupported": "Trình duyệt không được hỗ trợ",
|
||||
"this_browser_does_not_support_passkeys": "Trình duyệt này không hỗ trợ passkeys. Vui lòng sử dụng phương thức đăng nhập khác.",
|
||||
"an_unknown_error_occurred": "Một lỗi không rõ đã xảy ra",
|
||||
"authentication_process_was_aborted": "Quá trình xác thực đã bị hủy bỏ",
|
||||
"error_occurred_with_authenticator": "Đã xảy ra lỗi với trình xác thực",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Thiết bị xác thực không hỗ trợ discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "Thiết bị xác thực không hỗ trợ khóa lưu trữ",
|
||||
"passkey_was_previously_registered": "Passkey này đã được đăng ký trước đó",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Thiết bị xác thực không hỗ trợ bất kỳ thuật toán nào trong số các thuật toán được yêu cầu",
|
||||
"authenticator_timed_out": "Thời gian chờ của trình xác thực đã hết hạn",
|
||||
"critical_error_occurred_contact_administrator": "Đã xảy ra lỗi nghiêm trọng. Vui lòng liên hệ với quản trị viên.",
|
||||
"sign_in_to": "Đăng nhập {name}",
|
||||
"client_not_found": "Không tìm thấy client.",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> muốn truy cập các thông tin sau:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Bạn có muốn đăng nhập vào <b>{client}</b> với tài khoản {appName} của bạn?",
|
||||
"email": "Email",
|
||||
"view_your_email_address": "Xem địa chỉ email của bạn",
|
||||
"profile": "Hồ sơ",
|
||||
"view_your_profile_information": "Xem thông tin hồ sơ của bạn",
|
||||
"groups": "Nhóm",
|
||||
"view_the_groups_you_are_a_member_of": "Xem các nhóm mà bạn là thành viên",
|
||||
"cancel": "Hủy",
|
||||
"sign_in": "Đăng nhập",
|
||||
"try_again": "Thử lại",
|
||||
"client_logo": "Logo của client",
|
||||
"sign_out": "Đăng xuất",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Bạn có muốn đăng xuất khỏi {appName} bằng tài khoản <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Đăng nhập vào {appName}",
|
||||
"please_try_to_sign_in_again": "Vui lòng đăng nhập lại.",
|
||||
"authenticate_with_passkey_to_access_account": "Xác thực tài khoản của bạn bằng passkey để truy cập tài khoản.",
|
||||
"authenticate": "Xác thực",
|
||||
"please_try_again": "Vui lòng thử lại.",
|
||||
"continue": "Tiếp tục",
|
||||
"alternative_sign_in": "Đăng nhập bằng cách khác",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Nếu bạn không thể sử dụng passkey, bạn có thể đăng nhập bằng một trong các phương thức sau.",
|
||||
"use_your_passkey_instead": "Sử dụng passkey?",
|
||||
"email_login": "Đăng nhập bằng email",
|
||||
"enter_a_login_code_to_sign_in": "Nhập mã đăng nhập để đăng nhập.",
|
||||
"request_a_login_code_via_email": "Yêu cầu mã đăng nhập qua email.",
|
||||
"go_back": "Quay lại",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Một email đã được gửi đến địa chỉ email đã cung cấp, nếu địa chỉ đó tồn tại trong hệ thống.",
|
||||
"enter_code": "Nhập mã",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Nhập địa chỉ email của bạn để nhận email có mã đăng nhập.",
|
||||
"your_email": "Email của bạn",
|
||||
"submit": "Gửi",
|
||||
"enter_the_code_you_received_to_sign_in": "Nhập mã bạn đã nhận để đăng nhập.",
|
||||
"code": "Mã",
|
||||
"invalid_redirect_url": "URL chuyển hướng không hợp lệ",
|
||||
"audit_log": "Nhật ký hoạt động",
|
||||
"users": "Người dùng",
|
||||
"user_groups": "Nhóm người dùng",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "Khóa API",
|
||||
"application_configuration": "Cấu hình ứng dụng",
|
||||
"settings": "Cài đặt",
|
||||
"update_pocket_id": "Cập nhật Pocket ID",
|
||||
"powered_by": "Được cung cấp bởi",
|
||||
"see_your_account_activities_from_the_last_3_months": "Xem các hoạt động tài khoản của bạn trong 3 tháng qua.",
|
||||
"time": "Thời gian",
|
||||
"event": "Sự kiện",
|
||||
"approximate_location": "Vị Trí Ước Tính",
|
||||
"ip_address": "Địa chỉ IP",
|
||||
"device": "Thiết bị",
|
||||
"client": "Client.",
|
||||
"unknown": "Không xác định",
|
||||
"account_details_updated_successfully": "Cập nhật tài khoản thành công",
|
||||
"profile_picture_updated_successfully": "Hình đại diện đã được cập nhật thành công. Việc cập nhật có thể mất vài phút.",
|
||||
"account_settings": "Cài Đặt Tài Khoản",
|
||||
"passkey_missing": "Passkey bị thiếu",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Vui lòng thêm passkey để tránh mất quyền truy cập vào tài khoản của bạn.",
|
||||
"single_passkey_configured": "Đã cài đặt passkey duy nhất",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Nên có hơn một passkey cập để tránh mất quyền truy cập vào tài khoản của bạn.",
|
||||
"account_details": "Thông Tin Tài Khoản",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Quản lý các passkeys mà bạn có thể sử dụng để xác thực danh tính của mình.",
|
||||
"add_passkey": "Thêm Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Tạo mã đăng nhập một lần để đăng nhập từ thiết bị khác mà không cần passkeys.",
|
||||
"create": "Tạo",
|
||||
"first_name": "Tên",
|
||||
"last_name": "Họ",
|
||||
"username": "Tên đăng nhập",
|
||||
"save": "Lưu",
|
||||
"username_can_only_contain": "Tên người dùng chỉ có thể chứa các ký tự chữ thường, số, dấu gạch dưới, dấu chấm, dấu gạch ngang và ký hiệu '@'.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Đăng nhập bằng mã sau. Mã này sẽ hết hạn trong 15 phút.",
|
||||
"or_visit": "hoặc truy cập",
|
||||
"added_on": "Đã được thêm vào",
|
||||
"rename": "Đổi tên",
|
||||
"delete": "Xóa",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Bạn có chắc chắn muốn xóa passkey này không?",
|
||||
"passkey_deleted_successfully": "Passkey đã được xóa thành công",
|
||||
"delete_passkey_name": "Xóa {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Tên passkey đã được cập nhật thành công",
|
||||
"name_passkey": "Tên Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Đặt tên cho passkey để dễ dàng nhận diện sau này.",
|
||||
"create_api_key": "Tạo API Key",
|
||||
"add_a_new_api_key_for_programmatic_access": "Thêm API Key mới cho truy cập lập trình.",
|
||||
"add_api_key": "Thêm API Key",
|
||||
"manage_api_keys": "Quản lý API keys",
|
||||
"api_key_created": "API Key đã được tạo",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Vì lý do bảo mật, khóa này chỉ được hiển thị một lần. Vui lòng lưu trữ nó một cách an toàn.",
|
||||
"description": "Mô tả",
|
||||
"api_key": "API Key",
|
||||
"close": "Đóng",
|
||||
"name_to_identify_this_api_key": "Tên để xác API key này.",
|
||||
"expires_at": "Hết hạn vào",
|
||||
"when_this_api_key_will_expire": "Thời điểm API Key này hết hạn.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Mô tả tùy chọn để giúp xác định mục đích của key này.",
|
||||
"expiration_date_must_be_in_the_future": "Ngày hết hạn phải nằm trong tương lai",
|
||||
"revoke_api_key": "Thu Hồi API Key",
|
||||
"never": "Không bao giờ",
|
||||
"revoke": "Thu hồi",
|
||||
"api_key_revoked_successfully": "API key đã được thu hồi thành công",
|
||||
"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",
|
||||
"actions": "Hành động",
|
||||
"images_updated_successfully": "Đã cập nhật hình ảnh thành công",
|
||||
"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.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Cấu hình cài đặt LDAP để đồng bộ hóa người dùng và nhóm từ máy chủ LDAP.",
|
||||
"images": "Hình ảnh",
|
||||
"update": "Cập nhật",
|
||||
"email_configuration_updated_successfully": "Cấu hình email đã được cập nhật thành công",
|
||||
"save_changes_question": "Lưu thay đổi?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Bạn phải lưu các thay đổi trước khi gửi email thử nghiệm. Bạn có muốn lưu ngay bây giờ không?",
|
||||
"save_and_send": "Lưu và gửi",
|
||||
"test_email_sent_successfully": "Email thử nghiệm đã được gửi thành công đến địa chỉ email của bạn.",
|
||||
"failed_to_send_test_email": "Không thể gửi email thử nghiệm. Vui lòng kiểm tra nhật ký máy chủ để biết thêm thông tin.",
|
||||
"smtp_configuration": "Cấu hình SMTP",
|
||||
"smtp_host": "Máy Chủ SMTP",
|
||||
"smtp_port": "Cổng SMTP",
|
||||
"smtp_user": "Người Dùng SMTP",
|
||||
"smtp_password": "Mật Khẩu SMTP",
|
||||
"smtp_from": "SMTP Gửi Từ Địa Chỉ Email",
|
||||
"smtp_tls_option": "Tùy Chọn TLS Cho SMTP",
|
||||
"email_tls_option": "Tùy Chọn TLS Cho email",
|
||||
"skip_certificate_verification": "Bỏ Qua Xác Minh Chứng Chỉ (Certificate Verification)",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Điều này có thể hữu ích cho chứng chỉ tự ký.",
|
||||
"enabled_emails": "Email đã bật",
|
||||
"email_login_notification": "Thông Báo Đăng Nhập Email",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Gửi email cho người dùng khi họ đăng nhập từ một thiết bị mới.",
|
||||
"emai_login_code_requested_by_user": "Mã Đăng Nhập Email Được Yêu Cầu Bởi Người Dùng",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Cho phép người dùng bỏ qua passkeys bằng cách yêu cầu mã đăng nhập được gửi đến email của họ. Điều này làm giảm đáng kể tính bảo mật vì bất kỳ ai có quyền truy cập vào email của người dùng đều có thể truy cập vào hệ thống.",
|
||||
"email_login_code_from_admin": "Mã Đăng Nhập Email Từ Quản Trị Viên",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Cho phép quản trị viên gửi mã đăng nhập cho người dùng qua email.",
|
||||
"send_test_email": "Gửi email thử nghiệm",
|
||||
"application_configuration_updated_successfully": "Cấu hình ứng dụng đã được cập nhật thành công",
|
||||
"application_name": "Tên Ứng Dụng",
|
||||
"session_duration": "Thời Lượng Phiên",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Thời gian của một phiên (tính bằng phút) trước khi người dùng phải đăng nhập lại.",
|
||||
"enable_self_account_editing": "Cho Phép Chỉnh Sửa Tài Khoản Cá Nhân",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Người dùng có nên được phép chỉnh sửa thông tin tài khoản của mình không?",
|
||||
"emails_verified": "Xác Minh Email",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Có nên đánh dấu email của người dùng là đã xác minh cho các OIDC clients hay không.",
|
||||
"ldap_configuration_updated_successfully": "Cấu hình LDAP đã được cập nhật thành công",
|
||||
"ldap_disabled_successfully": "Tắt LDAP thành công",
|
||||
"ldap_sync_finished": "Quá trình đồng bộ hóa LDAP đã hoàn tất",
|
||||
"client_configuration": "Cấu Hình Client",
|
||||
"ldap_url": "URL Của LDAP",
|
||||
"ldap_bind_dn": "DN Của LDAP Bind",
|
||||
"ldap_bind_password": "Mật Khẩu Của LDAP Bind",
|
||||
"ldap_base_dn": "Base DN Của LDAP",
|
||||
"user_search_filter": "Bộ Lọc Tìm Kiếm Người Dùng",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Bộ lọc tìm kiếm để sử dụng khi tìm kiếm hoặc đồng bộ hóa người dùng.",
|
||||
"groups_search_filter": "Bộ lọc tìm kiếm nhóm",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Bộ lọc tìm kiếm để sử dụng khi tìm kiếm hoặc đồng bộ hóa nhóm.",
|
||||
"attribute_mapping": "Map Thuộc Tính",
|
||||
"user_unique_identifier_attribute": "Thuộc tính định danh duy nhất của người dùng",
|
||||
"the_value_of_this_attribute_should_never_change": "Giá trị của thuộc tính này không bao giờ được thay đổi.",
|
||||
"username_attribute": "Thuộc tính tên đăng nhập",
|
||||
"user_mail_attribute": "Thuộc tính email của người dùng",
|
||||
"user_first_name_attribute": "Thuộc tính tên của người dùng",
|
||||
"user_last_name_attribute": "Thuộc tính họ của người dùng",
|
||||
"user_profile_picture_attribute": "Thuộc tính hình ảnh hồ sơ người dùng",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Giá trị của thuộc tính này có thể là một URL, một tệp nhị phân hoặc một hình ảnh được mã hóa base64.",
|
||||
"group_members_attribute": "Thuộc tính thành viên nhóm",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Thuộc tính được sử dụng để truy vấn các thành viên của một nhóm.",
|
||||
"group_unique_identifier_attribute": "Thuộc tính định danh duy nhất của người dùng",
|
||||
"group_name_attribute": "Thuộc tính tên nhóm",
|
||||
"admin_group_name": "Tên nhóm quản trị",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Các thành viên của nhóm này sẽ có quyền quản trị trong Pocket ID.",
|
||||
"disable": "Tắt",
|
||||
"sync_now": "Đồng bộ hóa ngay",
|
||||
"enable": "Bật",
|
||||
"user_created_successfully": "Đã tạo người dùng thành công",
|
||||
"create_user": "Tạo người dùng",
|
||||
"add_a_new_user_to_appname": "Thêm người dùng mới vào {appName}",
|
||||
"add_user": "Thêm Người Dùng",
|
||||
"manage_users": "Quản Lý Người Dùng",
|
||||
"admin_privileges": "Quyền quản trị viên",
|
||||
"admins_have_full_access_to_the_admin_panel": "Quản trị viên có quyền truy cập đầy đủ vào bảng điều khiển quản trị.",
|
||||
"delete_firstname_lastname": "Xóa {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Bạn có chắc chắn muốn xóa người này?",
|
||||
"user_deleted_successfully": "Đã xóa người dùng",
|
||||
"role": "Vai trò",
|
||||
"source": "Nguồn",
|
||||
"admin": "Quản trị viên",
|
||||
"user": "Người dùng",
|
||||
"local": "Cục Bộ",
|
||||
"toggle_menu": "Bật/tắt menu",
|
||||
"edit": "Chỉnh sửa",
|
||||
"user_groups_updated_successfully": "Nhóm người dùng đã được cập nhật thành công",
|
||||
"user_updated_successfully": "Đã cập nhật người dùng thành công",
|
||||
"custom_claims_updated_successfully": "Custom claims đã được cập nhật thành công",
|
||||
"back": "Quay Lại",
|
||||
"user_details_firstname_lastname": "Thông tin người dùng {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Quản lý các nhóm mà người dùng này thuộc về.",
|
||||
"custom_claims": "Custom Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims là các cặp key-value có thể được sử dụng để lưu trữ thông tin bổ sung về một người dùng. Các claims này sẽ được bao gồm trong token ID nếu phạm vi 'profile' được yêu cầu.",
|
||||
"user_group_created_successfully": "Nhóm người dùng đã được tạo thành công",
|
||||
"create_user_group": "Tạo Nhóm Người Dùng",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Tạo một nhóm mới có thể được gán cho người dùng.",
|
||||
"add_group": "Thêm Nhóm",
|
||||
"manage_user_groups": "Quản lý Nhóm Người dùng",
|
||||
"friendly_name": "Tên thân thiện",
|
||||
"name_that_will_be_displayed_in_the_ui": "Tên sẽ hiển thị trong giao diện người dùng",
|
||||
"name_that_will_be_in_the_groups_claim": "Tên sẽ hiển thị trong claim của phần \"nhóm\"",
|
||||
"delete_name": "Xoá {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Bạn có chắc chắn muốn xoá nhóm người dùng không?",
|
||||
"user_group_deleted_successfully": "Nhóm người dùng đã được xóa thành công",
|
||||
"user_count": "Số người dùng",
|
||||
"user_group_updated_successfully": "Nhóm người dùng đã được cập nhật thành công",
|
||||
"users_updated_successfully": "Đã cập nhật người dùng thành công",
|
||||
"user_group_details_name": "Chi tiết nhóm người dùng {name}",
|
||||
"assign_users_to_this_group": "Gán người dùng vào nhóm này.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims là các cặp key-value trị có thể được sử dụng để lưu trữ thông tin bổ sung về người dùng. Các yêu cầu này sẽ được bao gồm trong token ID nếu phạm vi 'profile' được yêu cầu. Các custom claims được định nghĩa trên người dùng sẽ được ưu tiên nếu có xung đột.",
|
||||
"oidc_client_created_successfully": "OIDC client đã được tạo thành công",
|
||||
"create_oidc_client": "Tạo OIDC client",
|
||||
"add_a_new_oidc_client_to_appname": "Thêm một OIDC client mới vào {appName}.",
|
||||
"add_oidc_client": "Thêm OIDC Client",
|
||||
"manage_oidc_clients": "Quản Lý OIDC Client",
|
||||
"one_time_link": "Liên kết sử dụng một lần",
|
||||
"use_this_link_to_sign_in_once": "Sử dụng liên kết này để đăng nhập một lần. Điều này là cần thiết cho người dùng chưa thêm passkey hoặc đã mất passkey.",
|
||||
"add": "Thêm",
|
||||
"callback_urls": "Callback URL",
|
||||
"logout_callback_urls": "Callback URL khi đăng xuất",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_description": "Public client không có client secret. Chúng được thiết kế cho các ứng dụng di động, web và ứng dụng gốc, nơi các khóa bí mật không thể được lưu trữ một cách an toàn.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange là một tính năng bảo mật nhằm ngăn chặn các cuộc tấn công CSRF và đánh cắp mã xác thực.",
|
||||
"requires_reauthentication": "Yêu cầu xác thực lại",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "Yêu cầu người dùng xác thực lại mỗi lần ủy quyền, ngay cả khi đã đăng nhập.",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Đổi Logo",
|
||||
"upload_logo": "Tải logo",
|
||||
"remove_logo": "Xóa logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Bạn có chắc chắn muốn xóa OIDC client này không?",
|
||||
"oidc_client_deleted_successfully": "OIDC client đã được xóa thành công",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "URL đăng xuất",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Bật",
|
||||
"disabled": "Tắt",
|
||||
"oidc_client_updated_successfully": "OIDC client đã được cập nhật thành công",
|
||||
"create_new_client_secret": "Tạo client secret mới",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Bạn có chắc chắn muốn tạo một client secret? Client secret cũ sẽ bị vô hiệu hóa.",
|
||||
"generate": "Tạo ra",
|
||||
"new_client_secret_created_successfully": "Client secret mới đã được tạo thành công",
|
||||
"allowed_user_groups_updated_successfully": "Các nhóm người dùng được phép đã được cập nhật thành công",
|
||||
"oidc_client_name": "OIDC Client {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Hiển thị thêm thông tin",
|
||||
"allowed_user_groups": "Nhóm người dùng được phép",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Thêm nhóm người dùng vào khách hàng này để hạn chế quyền truy cập cho người dùng trong các nhóm này. Nếu không chọn nhóm người dùng nào, tất cả người dùng sẽ có quyền truy cập vào client này.",
|
||||
"favicon": "Biểu tượng favicon",
|
||||
"light_mode_logo": "Logo cho Light Mode",
|
||||
"dark_mode_logo": "Logo cho Dark Mode",
|
||||
"background_image": "Ảnh nền",
|
||||
"language": "Ngôn ngữ",
|
||||
"reset_profile_picture_question": "Đặt lại ảnh đại diện?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Điều này sẽ xóa hình ảnh đã tải lên và đặt lại ảnh đại diện về mặc định. Bạn có muốn tiếp tục không?",
|
||||
"reset": "Khôi phục",
|
||||
"reset_to_default": "Khôi phục về mặc định",
|
||||
"profile_picture_has_been_reset": "Hình đại diện đã được đặt lại. Việc cập nhật có thể mất vài phút.",
|
||||
"select_the_language_you_want_to_use": "Chọn ngôn ngữ bạn muốn sử dụng. Lưu ý rằng một số văn bản có thể được dịch tự động và có thể không chính xác.",
|
||||
"contribute_to_translation": "Nếu bạn phát hiện vấn đề, bạn có thể đóng góp vào việc dịch trên <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||
"personal": "Cá nhân",
|
||||
"global": "Toàn cầu",
|
||||
"all_users": "Tất cả người dùng",
|
||||
"all_events": "Tất cả các sự kiện",
|
||||
"all_clients": "Tất cả clients",
|
||||
"all_locations": "Tất cả địa điểm",
|
||||
"global_audit_log": "Nhật ký kiểm tra toàn cầu",
|
||||
"see_all_account_activities_from_the_last_3_months": "Xem tất cả hoạt động của người dùng trong 3 tháng qua.",
|
||||
"token_sign_in": "Đăng nhập bằng token",
|
||||
"client_authorization": "Xác thực client",
|
||||
"new_client_authorization": "Xác thực client mới",
|
||||
"disable_animations": "Tắt hiệu ứng động",
|
||||
"turn_off_ui_animations": "Tắt các hiệu ứng động trong giao diện người dùng.",
|
||||
"user_disabled": "Tài khoản bị vô hiệu hóa",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Người dùng bị vô hiệu hóa không thể đăng nhập hoặc sử dụng dịch vụ.",
|
||||
"user_disabled_successfully": "Người dùng đã bị vô hiệu hóa thành công.",
|
||||
"user_enabled_successfully": "Người dùng đã được kích hoạt thành công.",
|
||||
"status": "Trạng thái",
|
||||
"disable_firstname_lastname": "Vô hiệu hóa ( {firstName} {lastName})",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Bạn có chắc chắn muốn vô hiệu hóa người dùng này không? Họ sẽ không thể đăng nhập hoặc truy cập bất kỳ dịch vụ nào.",
|
||||
"ldap_soft_delete_users": "Giữ người dùng bị vô hiệu hóa khỏi LDAP.",
|
||||
"ldap_soft_delete_users_description": "Khi được kích hoạt, người dùng bị xóa khỏi LDAP sẽ bị vô hiệu hóa thay vì bị xóa khỏi hệ thống.",
|
||||
"login_code_email_success": "Mã đăng nhập đã được gửi đến người dùng.",
|
||||
"send_email": "Gửi Email",
|
||||
"show_code": "Hiện Thị Mã",
|
||||
"callback_url_description": "URL do client của bạn cung cấp. Sẽ được tự động thêm nếu để trống. Hỗ trợ ký tự đại diện (*), nhưng nên tránh sử dụng để đảm bảo an toàn.",
|
||||
"logout_callback_url_description": "URL do client của bạn cung cấp để đăng xuất. Ký tự đại diện (*) được hỗ trợ, nhưng nên tránh sử dụng để đảm bảo an toàn.",
|
||||
"api_key_expiration": "Hạn sử dụng API Key",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Gửi email cho người dùng khi API key của họ sắp hết hạn.",
|
||||
"authorize_device": "Thiết Bị Được Cho Phép",
|
||||
"the_device_has_been_authorized": "Thiết bị đã được cấp quyền.",
|
||||
"enter_code_displayed_in_previous_step": "Nhập mã đã hiển thị ở bước trước.",
|
||||
"authorize": "Cho phép",
|
||||
"federated_client_credentials": "Thông Tin Xác Thực Của Federated Clients",
|
||||
"federated_client_credentials_description": "Sử dụng thông tin xác thực của federated client, bạn có thể xác thực các client OIDC bằng cách sử dụng token JWT được cấp bởi các bên thứ ba.",
|
||||
"add_federated_client_credential": "Thêm thông tin xác thực cho federated clients",
|
||||
"add_another_federated_client_credential": "Thêm một thông tin xác thực cho federated clients khác",
|
||||
"oidc_allowed_group_count": "Số lượng nhóm được phép",
|
||||
"unrestricted": "Không hạn chế",
|
||||
"show_advanced_options": "Hiển thị tuỳ chọn nâng cao",
|
||||
"hide_advanced_options": "Ẩn tuỳ chọn nâng cao",
|
||||
"oidc_data_preview": "Xem trước dữ liệu OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Xem trước dữ liệu OIDC sẽ được gửi cho các người dùng khác nhau",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Sao chép",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Sao chép tất cả",
|
||||
"preview": "Xem trước",
|
||||
"preview_for_user": "Xem trước cho {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Xem trước dữ liệu OIDC sẽ được gửi cho người dùng này",
|
||||
"show": "Hiển thị",
|
||||
"select_an_option": "Chọn một tùy chọn",
|
||||
"select_user": "Chọn người dùng",
|
||||
"error": "Lỗi",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Chọn màu nhấn để tùy chỉnh giao diện của Pocket ID.",
|
||||
"accent_color": "Màu nhấn",
|
||||
"custom_accent_color": "Tuỳ chỉnh màu sắc",
|
||||
"custom_accent_color_description": "Nhập màu tùy chỉnh bằng định dạng màu CSS hợp lệ (ví dụ: hex, rgb, hsl).",
|
||||
"color_value": "Giá trị màu",
|
||||
"apply": "Áp dụng",
|
||||
"signup_token": "Token đăng ký",
|
||||
"create_a_signup_token_to_allow_new_user_registration": "Tạo token đăng ký để cho phép người dùng mới đăng ký.",
|
||||
"usage_limit": "Giới hạn sử dụng",
|
||||
"number_of_times_token_can_be_used": "Số lần token đăng ký có thể được sử dụng.",
|
||||
"expires": "Hết hạn",
|
||||
"signup": "Đăng ký",
|
||||
"user_creation": "Tạo tài khoản người dùng",
|
||||
"configure_user_creation": "Quản lý cài đặt tạo tài khoản người dùng, bao gồm các phương thức đăng ký và quyền truy cập mặc định cho người dùng mới.",
|
||||
"user_creation_groups_description": "Tự động gán các nhóm này cho người dùng mới khi họ đăng ký.",
|
||||
"user_creation_claims_description": "Tự động gán các yêu cầu tùy chỉnh này cho người dùng mới khi họ đăng ký.",
|
||||
"user_creation_updated_successfully": "Cài đặt tạo người dùng đã được cập nhật thành công.",
|
||||
"signup_disabled_description": "Đăng ký người dùng đã bị vô hiệu hóa hoàn toàn. Chỉ quản trị viên mới có thể tạo tài khoản người dùng mới.",
|
||||
"signup_requires_valid_token": "Yêu cầu mã đăng ký hợp lệ để tạo tài khoản",
|
||||
"validating_signup_token": "Xác thực token đăng ký",
|
||||
"go_to_login": "Đi tới đăng nhập",
|
||||
"signup_to_appname": "Đăng ký vào {appName}",
|
||||
"create_your_account_to_get_started": "Tạo tài khoản của bạn để bắt đầu.",
|
||||
"initial_account_creation_description": "Vui lòng tạo tài khoản của bạn để bắt đầu. Bạn có thể thiết lập passkey sau này.",
|
||||
"setup_your_passkey": "Thiết lập passkey của bạn",
|
||||
"create_a_passkey_to_securely_access_your_account": "Tạo passkey để truy cập tài khoản của bạn một cách an toàn. Đây sẽ là phương thức chính để bạn đăng nhập.",
|
||||
"skip_for_now": "Tạm thời bỏ qua",
|
||||
"account_created": "Tài khoản đã được tạo",
|
||||
"enable_user_signups": "Bật đăng ký người dùng",
|
||||
"enable_user_signups_description": "Quyết định cách người dùng có thể đăng ký tài khoản mới trong Pocket ID.",
|
||||
"user_signups_are_disabled": "Đăng ký người dùng hiện đang bị vô hiệu hóa",
|
||||
"create_signup_token": "Tạo Signup Token",
|
||||
"view_active_signup_tokens": "Xem các Signup Tokens đang hoạt động",
|
||||
"manage_signup_tokens": "Quản lý Signup Tokén",
|
||||
"view_and_manage_active_signup_tokens": "Xem và quản lý các Signup Tokén đang hoạt động.",
|
||||
"signup_token_deleted_successfully": "Signup token đã bị xóa thành công.",
|
||||
"expired": "Đã hết hạn",
|
||||
"used_up": "Đã Dùng Hết",
|
||||
"active": "Hoạt động",
|
||||
"usage": "Độ sử dụng",
|
||||
"created": "Ngày tạo",
|
||||
"token": "Token",
|
||||
"loading": "Đang tải",
|
||||
"delete_signup_token": "Xóa mã Signup Token",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Bạn có chắc muốn xoá signup token này không? Thao tác này không thể được hoàn lại.",
|
||||
"signup_with_token": "Đăng ký bằng token",
|
||||
"signup_with_token_description": "Người dùng chỉ có thể đăng ký bằng mã đăng ký hợp lệ do quản trị viên tạo ra.",
|
||||
"signup_open": "Mở Đăng Ký",
|
||||
"signup_open_description": "Bất kỳ ai cũng có thể tạo tài khoản mới mà không có bất kỳ hạn chế nào.",
|
||||
"of": "của",
|
||||
"skip_passkey_setup": "Bỏ qua thiết lập Passkey",
|
||||
"skip_passkey_setup_description": "Bạn nên thiết lập khóa truy cập (passkey) vì nếu không có, bạn sẽ bị khóa khỏi tài khoản ngay khi phiên làm việc hết hạn.",
|
||||
"my_apps": "Ứng dụng của tôi",
|
||||
"no_apps_available": "Không có ứng dụng nào có sẵn.",
|
||||
"contact_your_administrator_for_app_access": "Liên hệ với quản trị viên của bạn để được cấp quyền truy cập vào các ứng dụng.",
|
||||
"launch": "Ra mắt",
|
||||
"client_launch_url": "URL khởi chạy của khách hàng",
|
||||
"client_launch_url_description": "Đường dẫn URL sẽ được mở khi người dùng khởi chạy ứng dụng từ trang Ứng dụng của tôi.",
|
||||
"client_name_description": "Tên của khách hàng hiển thị trong giao diện Pocket ID.",
|
||||
"revoke_access": "Hủy quyền truy cập",
|
||||
"revoke_access_description": "Hủy quyền truy cập vào <b>{clientName}</b>. <b>{clientName}</b> sẽ không còn có thể truy cập thông tin tài khoản của bạn.",
|
||||
"revoke_access_successful": "Quyền truy cập vào {clientName} đã bị thu hồi thành công.",
|
||||
"last_signed_in_ago": "Lần đăng nhập cuối cùng cách đây {time}",
|
||||
"invalid_client_id": "ID khách hàng chỉ có thể chứa các ký tự chữ cái, số, dấu gạch dưới và dấu gạch ngang.",
|
||||
"custom_client_id_description": "Đặt ID khách hàng tùy chỉnh nếu ứng dụng của bạn yêu cầu. Nếu không, hãy để trống để hệ thống tự động tạo một ID ngẫu nhiên.",
|
||||
"generated": "Được tạo ra"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"value": "键值",
|
||||
"remove_custom_claim": "删除自定义声明",
|
||||
"add_custom_claim": "添加自定义声明",
|
||||
"add_another": "添加另一个",
|
||||
"add_another": "再添加一个",
|
||||
"select_a_date": "选择日期",
|
||||
"select_file": "选择上传文件",
|
||||
"profile_picture": "头像",
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "点击复制",
|
||||
"something_went_wrong": "出了点问题",
|
||||
"go_back_to_home": "返回首页",
|
||||
"dont_have_access_to_your_passkey": "无法使用您的通行密钥?试试其他登录方式。",
|
||||
"alternative_sign_in_methods": "替代登录方式",
|
||||
"login_background": "登录页背景图",
|
||||
"logo": "图标",
|
||||
"login_code": "临时登录码",
|
||||
@@ -43,7 +43,7 @@
|
||||
"error_occurred_with_authenticator": "认证器发生错误",
|
||||
"authenticator_does_not_support_discoverable_credentials": "认证器不支持可发现的凭据",
|
||||
"authenticator_does_not_support_resident_keys": "认证器不支持常驻密钥",
|
||||
"passkey_was_previously_registered": "此通行密钥之前已注册",
|
||||
"passkey_was_previously_registered": "此通行密钥曾被注册",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "认证器不支持任何请求的算法",
|
||||
"authenticator_timed_out": "认证器超时",
|
||||
"critical_error_occurred_contact_administrator": "发生严重错误。请联系您的管理员。",
|
||||
@@ -92,7 +92,7 @@
|
||||
"application_configuration": "设置",
|
||||
"settings": "设置",
|
||||
"update_pocket_id": "更新 Pocket ID",
|
||||
"powered_by": "由",
|
||||
"powered_by": "",
|
||||
"see_your_account_activities_from_the_last_3_months": "查看过去 3 个月的账户活动。",
|
||||
"time": "时间",
|
||||
"event": "事件",
|
||||
@@ -143,7 +143,7 @@
|
||||
"expires_at": "到期时间",
|
||||
"when_this_api_key_will_expire": "此 API 密钥的到期时间。",
|
||||
"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 密钥",
|
||||
"never": "永不",
|
||||
"revoke": "撤销",
|
||||
@@ -173,7 +173,7 @@
|
||||
"smtp_tls_option": "SMTP TLS 选项",
|
||||
"email_tls_option": "电子邮件 TLS 选项",
|
||||
"skip_certificate_verification": "跳过证书验证",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "这对于自签名证书很有用。",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "这对于自签证书很有用。",
|
||||
"enabled_emails": "启用的电子邮件",
|
||||
"email_login_notification": "登录时的电子邮件通知",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "用户通过新设备登录时发送一封电子邮件通知。",
|
||||
@@ -195,9 +195,9 @@
|
||||
"ldap_sync_finished": "LDAP 同步完成",
|
||||
"client_configuration": "客户端配置",
|
||||
"ldap_url": "LDAP 地址",
|
||||
"ldap_bind_dn": "LDAP 绑定用户专有名称",
|
||||
"ldap_bind_dn": "LDAP 绑定 DN",
|
||||
"ldap_bind_password": "LDAP 绑定密码",
|
||||
"ldap_base_dn": "LDAP 基础用户专有名称",
|
||||
"ldap_base_dn": "LDAP 基础 DN",
|
||||
"user_search_filter": "用户搜索过滤器",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "用于搜索或同步用户的筛选器。",
|
||||
"groups_search_filter": "群组搜索过滤器",
|
||||
@@ -270,12 +270,14 @@
|
||||
"one_time_link": "一次性链接",
|
||||
"use_this_link_to_sign_in_once": "使用此链接进行一次性登录。这对尚未添加或已丢失通行密钥的用户来说非常必要。",
|
||||
"add": "添加",
|
||||
"callback_urls": "Callback URL",
|
||||
"logout_callback_urls": "Logout Callback URL",
|
||||
"callback_urls": "回调 URL",
|
||||
"logout_callback_urls": "登出回调URL",
|
||||
"public_client": "公共客户端",
|
||||
"public_clients_description": "公共客户端没有客户端密钥。它们用于无法安全存储密钥的移动端、Web端和原生应用程序。",
|
||||
"pkce": "PKCE",
|
||||
"pkce": "公钥代码交换",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
|
||||
"requires_reauthentication": "需要重新验证",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "要求用户在每次授权时重新进行身份验证,即使用户已登录。",
|
||||
"name_logo": "{name} Logo",
|
||||
"change_logo": "更改 Logo",
|
||||
"upload_logo": "上传 Logo",
|
||||
@@ -350,7 +352,7 @@
|
||||
"federated_client_credentials": "联合身份",
|
||||
"federated_client_credentials_description": "您可以使用联合身份,通过第三方授权机构签发的 JWT 令牌,对 OIDC 客户端进行认证。",
|
||||
"add_federated_client_credential": "添加联合身份",
|
||||
"add_another_federated_client_credential": "添加另一个联合身份",
|
||||
"add_another_federated_client_credential": "再添加一个联合身份",
|
||||
"oidc_allowed_group_count": "允许的群组数量",
|
||||
"unrestricted": "不受限制",
|
||||
"show_advanced_options": "显示高级选项",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "注册令牌最多使用次数。",
|
||||
"expires": "过期时间",
|
||||
"signup": "注册",
|
||||
"user_creation": "用户创建",
|
||||
"configure_user_creation": "管理用户创建设置,包括注册方式和新用户的默认权限。",
|
||||
"user_creation_groups_description": "在用户注册时自动将这些组分配给新用户。",
|
||||
"user_creation_claims_description": "在用户注册时自动为新用户分配这些自定义声明。",
|
||||
"user_creation_updated_successfully": "用户创建设置已成功更新。",
|
||||
"signup_disabled_description": "已完全禁止新用户注册。只有管理员可以创建新账户。",
|
||||
"signup_requires_valid_token": "必须输入有效注册令牌才能注册新账户",
|
||||
"validating_signup_token": "正在验证注册令牌",
|
||||
"go_to_login": "跳转到登录界面",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "暂时跳过",
|
||||
"account_created": "账户已创建",
|
||||
"enable_user_signups": "允许用户注册",
|
||||
"enable_user_signups_description": "是否启用新用户注册功能。",
|
||||
"enable_user_signups_description": "决定用户如何在Pocket ID中注册新账户。",
|
||||
"user_signups_are_disabled": "目前禁止新用户注册",
|
||||
"create_signup_token": "创建一个注册令牌",
|
||||
"view_active_signup_tokens": "查看有效注册令牌",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "正在加载",
|
||||
"delete_signup_token": "删除注册令牌",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "确定要删除这个注册令牌吗?此操作不可撤销。",
|
||||
"signup_disabled_description": "已完全禁止新用户注册。只有管理员可以创建新账户。",
|
||||
"signup_with_token": "使用令牌注册",
|
||||
"signup_with_token_description": "用户必须持有管理员创建的有效注册令牌才能注册新账户。",
|
||||
"signup_open": "开放注册",
|
||||
"signup_open_description": "任何人都可以无限制地注册新账户。",
|
||||
"of": "中的",
|
||||
"skip_passkey_setup": "跳过设置通行密钥",
|
||||
"skip_passkey_setup_description": "强烈建议设置一个通行密钥,否则您在此会话结束后将无法访问您的账户。"
|
||||
"skip_passkey_setup_description": "强烈建议设置一个通行密钥,否则您在此会话结束后将无法访问您的账户。",
|
||||
"my_apps": "我的应用程序",
|
||||
"no_apps_available": "没有可用应用程序",
|
||||
"contact_your_administrator_for_app_access": "请联系您的管理员以获取应用程序的访问权限。",
|
||||
"launch": "启动",
|
||||
"client_launch_url": "客户发布链接",
|
||||
"client_launch_url_description": "当用户从“我的应用”页面启动应用时将打开的 URL。",
|
||||
"client_name_description": "在Pocket ID用户界面中显示的客户端名称。",
|
||||
"revoke_access": "撤销访问权限",
|
||||
"revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。",
|
||||
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。",
|
||||
"last_signed_in_ago": "最后一次登录 {time} 前",
|
||||
"invalid_client_id": "客户 ID 只能包含字母、数字、下划线和连字符。",
|
||||
"custom_client_id_description": "如果您的应用程序需要自定义客户端 ID,请在此处设置。否则,请留空以生成一个随机生成的客户端 ID。",
|
||||
"generated": "生成"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"click_to_copy": "點擊以複製",
|
||||
"something_went_wrong": "出了點問題",
|
||||
"go_back_to_home": "返回首頁",
|
||||
"dont_have_access_to_your_passkey": "無法存取您的密碼金鑰嗎?",
|
||||
"alternative_sign_in_methods": "其他登入方法",
|
||||
"login_background": "登入背景",
|
||||
"logo": "標誌",
|
||||
"login_code": "登入代碼",
|
||||
@@ -276,6 +276,8 @@
|
||||
"public_clients_description": "公開客戶端 (Public Client) 不包含 client secret。這類客戶端是為了行動裝置、網頁以及無法安全儲存 secret 的原生應用程式所設計。",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "PKCE(公開金鑰碼交換)是一項安全機制,用於防止 CSRF 與授權碼攔截攻擊。",
|
||||
"requires_reauthentication": "需要重新驗證",
|
||||
"requires_users_to_authenticate_again_on_each_authorization": "要求使用者在每次授權時再次驗證,即使已經登入也是如此",
|
||||
"name_logo": "{name} 標誌",
|
||||
"change_logo": "更改標誌",
|
||||
"upload_logo": "上傳標誌",
|
||||
@@ -385,6 +387,12 @@
|
||||
"number_of_times_token_can_be_used": "註冊令牌可使用的次數。",
|
||||
"expires": "到期",
|
||||
"signup": "註冊",
|
||||
"user_creation": "使用者建立",
|
||||
"configure_user_creation": "管理使用者建立設定,包括註冊方法和新使用者的預設權限。",
|
||||
"user_creation_groups_description": "在新使用者註冊時自動指定這些群組。",
|
||||
"user_creation_claims_description": "在新使用者註冊時自動將這些自訂索賠指派給他們。",
|
||||
"user_creation_updated_successfully": "使用者建立設定更新成功。",
|
||||
"signup_disabled_description": "使用者註冊已完全停用。只有管理員可以建立新的使用者帳號。",
|
||||
"signup_requires_valid_token": "建立帳號需要有效的註冊令牌",
|
||||
"validating_signup_token": "驗證註冊標記",
|
||||
"go_to_login": "前往登入",
|
||||
@@ -396,7 +404,7 @@
|
||||
"skip_for_now": "暫時跳過",
|
||||
"account_created": "帳號已建立",
|
||||
"enable_user_signups": "啟用使用者註冊",
|
||||
"enable_user_signups_description": "是否要啟用使用者註冊功能。",
|
||||
"enable_user_signups_description": "決定使用者如何在 Pocket ID 中註冊新帳戶。",
|
||||
"user_signups_are_disabled": "使用者註冊目前已停用",
|
||||
"create_signup_token": "建立註冊令牌",
|
||||
"view_active_signup_tokens": "檢視有效的註冊令牌",
|
||||
@@ -412,12 +420,25 @@
|
||||
"loading": "載入中",
|
||||
"delete_signup_token": "刪除註冊令牌",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "您確定要刪除這個註冊令牌嗎?此動作無法撤銷。",
|
||||
"signup_disabled_description": "使用者註冊已完全停用。只有管理員可以建立新的使用者帳號。",
|
||||
"signup_with_token": "使用註冊令牌註冊",
|
||||
"signup_with_token_description": "使用者只能使用管理員建立的有效登入令牌註冊。",
|
||||
"signup_open": "開放報名",
|
||||
"signup_open_description": "任何人都可以不受限制地建立新帳號。",
|
||||
"of": "的",
|
||||
"skip_passkey_setup": "跳過密碼金鑰設定",
|
||||
"skip_passkey_setup_description": "我們強烈建議您設定密碼金鑰,因為如果沒有密碼金鑰,當工作階段到期時,您就會被鎖住。"
|
||||
"skip_passkey_setup_description": "我們強烈建議您設定密碼金鑰,因為如果沒有密碼金鑰,當工作階段到期時,您就會被鎖住。",
|
||||
"my_apps": "我的應用程式",
|
||||
"no_apps_available": "沒有可用的應用程式",
|
||||
"contact_your_administrator_for_app_access": "聯絡您的管理員以取得應用程式的存取權。",
|
||||
"launch": "啟動",
|
||||
"client_launch_url": "用戶端啟動 URL",
|
||||
"client_launch_url_description": "當使用者從「我的應用程式」頁面啟動應用程式時將會開啟的 URL。",
|
||||
"client_name_description": "顯示在 Pocket ID UI 中的用戶端名稱。",
|
||||
"revoke_access": "撤銷存取權",
|
||||
"revoke_access_description": "撤銷存取 <b>{clientName}</b>. <b>{clientName}</b>將無法再存取您的帳戶資訊。",
|
||||
"revoke_access_successful": "{clientName} 的存取權已成功取消。",
|
||||
"last_signed_in_ago": "上次登入 {time} 前",
|
||||
"invalid_client_id": "用戶端 ID 只能包含字母、數字、底線和連字符",
|
||||
"custom_client_id_description": "如果您的應用程式需要,請設定自訂用戶端 ID。否則,請留空以產生隨機 ID。",
|
||||
"generated": "產生"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user