mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-28 18:26:36 +00:00
Compare commits
45 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 |
22
.github/ISSUE_TEMPLATE/bug.yml
vendored
22
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -36,13 +36,29 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
### Additional Information
|
### Additional Information
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: extra-information
|
id: version
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: "Version and Environment"
|
label: "Pocket ID Version"
|
||||||
description: "Please specify the version of Pocket ID, along with any environment-specific configurations, such your reverse proxy, that might be relevant."
|
description: "Please specify the version of Pocket ID."
|
||||||
placeholder: "e.g., v0.24.1"
|
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
|
- type: textarea
|
||||||
id: log-files
|
id: log-files
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
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
|
go-version-file: backend/go.mod
|
||||||
|
|
||||||
- name: Run Golangci-lint
|
- name: Run Golangci-lint
|
||||||
uses: golangci/golangci-lint-action@dec74fa03096ff515422f71d18d41307cacde373 # v7.0.0
|
uses: golangci/golangci-lint-action@v8.0.0
|
||||||
with:
|
with:
|
||||||
version: v2.0.2
|
version: v2.4.0
|
||||||
args: --build-tags=exclude_frontend
|
args: --build-tags=exclude_frontend
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
only-new-issues: ${{ github.event_name == 'pull_request' }}
|
only-new-issues: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|||||||
141
.github/workflows/e2e-tests.yml
vendored
141
.github/workflows/e2e-tests.yml
vendored
@@ -3,15 +3,15 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'docs/**'
|
- "docs/**"
|
||||||
- '**.md'
|
- "**.md"
|
||||||
- '.github/**'
|
- ".github/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'docs/**'
|
- "docs/**"
|
||||||
- '**.md'
|
- "**.md"
|
||||||
- '.github/**'
|
- ".github/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -45,23 +45,29 @@ jobs:
|
|||||||
path: /tmp/docker-image.tar
|
path: /tmp/docker-image.tar
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
test-sqlite:
|
test:
|
||||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: write
|
actions: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
db: [sqlite, postgres]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: "pnpm"
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Cache Playwright Browsers
|
- name: Cache Playwright Browsers
|
||||||
@@ -70,100 +76,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: ~/.cache/ms-playwright
|
||||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-playwright-
|
|
||||||
|
|
||||||
- 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: 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: Install test dependencies
|
|
||||||
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: pnpm dlx playwright install --with-deps chromium
|
|
||||||
|
|
||||||
- name: Run Docker Container with Sqlite DB and LDAP
|
|
||||||
working-directory: ./tests/setup
|
|
||||||
run: |
|
|
||||||
docker compose up -d
|
|
||||||
docker compose logs -f pocket-id &> /tmp/backend.log &
|
|
||||||
|
|
||||||
- name: Run Playwright tests
|
|
||||||
working-directory: tests
|
|
||||||
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
|
|
||||||
path: 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-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
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
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('pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-playwright-
|
|
||||||
|
|
||||||
- name: Cache PostgreSQL Docker image
|
- name: Cache PostgreSQL Docker image
|
||||||
|
if: matrix.db == 'postgres'
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
id: postgres-cache
|
id: postgres-cache
|
||||||
with:
|
with:
|
||||||
@@ -171,15 +85,14 @@ jobs:
|
|||||||
key: postgres-17-${{ runner.os }}
|
key: postgres-17-${{ runner.os }}
|
||||||
|
|
||||||
- name: Pull and save PostgreSQL image
|
- name: Pull and save PostgreSQL image
|
||||||
if: steps.postgres-cache.outputs.cache-hit != 'true'
|
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
docker pull postgres:17
|
docker pull postgres:17
|
||||||
docker save postgres:17 > /tmp/postgres-image.tar
|
docker save postgres:17 > /tmp/postgres-image.tar
|
||||||
|
|
||||||
- name: Load PostgreSQL image from cache
|
- name: Load PostgreSQL image from cache
|
||||||
if: steps.postgres-cache.outputs.cache-hit == 'true'
|
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit == 'true'
|
||||||
run: docker load < /tmp/postgres-image.tar
|
run: docker load < /tmp/postgres-image.tar
|
||||||
|
|
||||||
- name: Cache LLDAP Docker image
|
- name: Cache LLDAP Docker image
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
id: lldap-cache
|
id: lldap-cache
|
||||||
@@ -196,7 +109,6 @@ jobs:
|
|||||||
- name: Load LLDAP image from cache
|
- name: Load LLDAP image from cache
|
||||||
if: steps.lldap-cache.outputs.cache-hit == 'true'
|
if: steps.lldap-cache.outputs.cache-hit == 'true'
|
||||||
run: docker load < /tmp/lldap-image.tar
|
run: docker load < /tmp/lldap-image.tar
|
||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -207,14 +119,21 @@ jobs:
|
|||||||
run: docker load -i /tmp/docker-image.tar
|
run: docker load -i /tmp/docker-image.tar
|
||||||
|
|
||||||
- name: Install test dependencies
|
- name: Install test dependencies
|
||||||
run: pnpm --filter pocket-id-tests install
|
run: pnpm --filter pocket-id-tests install --frozen-lockfile
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
working-directory: ./tests
|
working-directory: ./tests
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: pnpm dlx playwright install --with-deps chromium
|
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 with Postgres DB and LDAP
|
- name: Run Docker Container (postgres) with LDAP
|
||||||
|
if: matrix.db == 'postgres'
|
||||||
working-directory: ./tests/setup
|
working-directory: ./tests/setup
|
||||||
run: |
|
run: |
|
||||||
docker compose -f docker-compose-postgres.yml up -d
|
docker compose -f docker-compose-postgres.yml up -d
|
||||||
@@ -228,8 +147,8 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
with:
|
with:
|
||||||
name: playwright-report-postgres
|
name: playwright-report-${{ matrix.db }}
|
||||||
path: frontend/tests/.report
|
path: tests/.report
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
retention-days: 15
|
retention-days: 15
|
||||||
|
|
||||||
@@ -237,7 +156,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
with:
|
with:
|
||||||
name: backend-postgres
|
name: backend-${{ matrix.db }}
|
||||||
path: /tmp/backend.log
|
path: /tmp/backend.log
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
retention-days: 15
|
retention-days: 15
|
||||||
|
|||||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,3 +1,50 @@
|
|||||||
|
## [](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)
|
## [](https://github.com/pocket-id/pocket-id/compare/v1.6.4...v) (2025-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
If you don't use Dev Containers, you need to install the following tools manually:
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/download/) >= 22
|
- [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)
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
### 2. Setup
|
### 2. Setup
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ COPY ./frontend ./frontend/
|
|||||||
RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
|
RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
|
||||||
|
|
||||||
# Stage 2: Build Backend
|
# Stage 2: Build Backend
|
||||||
FROM golang:1.24-alpine AS backend-builder
|
FROM golang:1.25-alpine AS backend-builder
|
||||||
ARG BUILD_TAGS
|
ARG BUILD_TAGS
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY ./backend/go.mod ./backend/go.sum ./
|
COPY ./backend/go.mod ./backend/go.sum ./
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
module github.com/pocket-id/pocket-id/backend
|
module github.com/pocket-id/pocket-id/backend
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v11 v11.3.1
|
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/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||||
github.com/emersion/go-smtp v0.21.3
|
github.com/emersion/go-smtp v0.21.3
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0
|
github.com/fxamacker/cbor/v2 v2.9.0
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/glebarez/go-sqlite v1.21.2
|
github.com/glebarez/go-sqlite v1.22.0
|
||||||
github.com/glebarez/sqlite v1.11.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-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/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/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-uuid v1.0.3
|
github.com/hashicorp/go-uuid v1.0.3
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2
|
github.com/lestrrat-go/httprc/v3 v3.0.0
|
||||||
github.com/lestrrat-go/jwx/v3 v3.0.1
|
github.com/lestrrat-go/jwx/v3 v3.0.10
|
||||||
github.com/lmittmann/tint v1.1.2
|
github.com/lmittmann/tint v1.1.2
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/orandin/slog-gorm v1.4.0
|
github.com/orandin/slog-gorm v1.4.0
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
|
||||||
github.com/samber/slog-gin v1.15.1
|
github.com/samber/slog-gin v1.15.1
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
@@ -43,55 +43,58 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/log v0.10.0
|
go.opentelemetry.io/otel/sdk/log v0.10.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||||
go.opentelemetry.io/otel/trace v1.37.0
|
go.opentelemetry.io/otel/trace v1.37.0
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/image v0.24.0
|
golang.org/x/image v0.30.0
|
||||||
golang.org/x/text v0.26.0
|
golang.org/x/text v0.28.0
|
||||||
golang.org/x/time v0.9.0
|
golang.org/x/time v0.12.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.30.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.12.10 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
github.com/disintegration/gift v1.1.2 // indirect
|
github.com/disintegration/gift v1.1.2 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||||
github.com/google/go-tpm v0.9.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/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // 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/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/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/option 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/lib/pq v1.10.9 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -99,7 +102,7 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
@@ -110,7 +113,8 @@ require (
|
|||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // 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
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
|
||||||
@@ -127,18 +131,19 @@ require (
|
|||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.14.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||||
golang.org/x/net v0.38.0 // indirect
|
golang.org/x/net v0.43.0 // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/oauth2 v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.33.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/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc 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/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
|
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/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.0 // indirect
|
modernc.org/sqlite v1.38.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
197
backend/go.sum
197
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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI=
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
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.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
|
||||||
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
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 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
||||||
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||||
@@ -54,22 +52,22 @@ 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/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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.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.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
||||||
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/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 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
github.com/go-ldap/ldap/v3 v3.4.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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -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/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
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/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||||
github.com/go-webauthn/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8=
|
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
|
||||||
github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us=
|
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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
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 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
|
||||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
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/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 h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
@@ -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/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -169,16 +173,18 @@ 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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
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 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY=
|
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE=
|
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
|
||||||
github.com/lestrrat-go/jwx/v3 v3.0.1 h1:fH3T748FCMbXoF9UXXNS9i0q6PpYyJZK/rKSbkt2guY=
|
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
|
||||||
github.com/lestrrat-go/jwx/v3 v3.0.1/go.mod h1:XP2WqxMOSzHSyf3pfibCcfsLqbomxakAnNqiuaH8nwo=
|
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 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
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 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
||||||
@@ -212,10 +218,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
|||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
|
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
|
||||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2 h1:jG+FaCBv3h6GD5F+oenTfe3+0NmX8sCKjni5k3A5Dek=
|
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.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -246,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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -254,13 +259,14 @@ 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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -318,29 +324,31 @@ 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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
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-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.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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
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.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.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.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.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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
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-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-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -352,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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -361,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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -375,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.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.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -387,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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
@@ -395,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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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-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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
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-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 h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
google.golang.org/genproto/googleapis/api v0.0.0-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 h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
google.golang.org/grpc v1.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.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -423,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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
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 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
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.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -445,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/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
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 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ import (
|
|||||||
"github.com/golang-migrate/migrate/v4/database"
|
"github.com/golang-migrate/migrate/v4/database"
|
||||||
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
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"
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
slogGorm "github.com/orandin/slog-gorm"
|
slogGorm "github.com/orandin/slog-gorm"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
@@ -20,6 +23,7 @@ import (
|
|||||||
gormLogger "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/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
sqliteutil "github.com/pocket-id/pocket-id/backend/internal/utils/sqlite"
|
sqliteutil "github.com/pocket-id/pocket-id/backend/internal/utils/sqlite"
|
||||||
"github.com/pocket-id/pocket-id/backend/resources"
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
)
|
)
|
||||||
@@ -58,8 +62,9 @@ func NewDatabase() (db *gorm.DB, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func migrateDatabase(driver database.Driver) error {
|
func migrateDatabase(driver database.Driver) error {
|
||||||
// Use the embedded migrations
|
// Embedded migrations via iofs
|
||||||
source, err := iofs.New(resources.FS, "migrations/"+string(common.EnvConfig.DbProvider))
|
path := "migrations/" + string(common.EnvConfig.DbProvider)
|
||||||
|
source, err := iofs.New(resources.FS, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create embedded migration source: %w", err)
|
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)
|
return fmt.Errorf("failed to create migration instance: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.Up()
|
requiredVersion, err := getRequiredMigrationVersion(path)
|
||||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to apply migrations: %w", err)
|
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
|
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) {
|
func connectDatabase() (db *gorm.DB, err error) {
|
||||||
var dialector gorm.Dialector
|
var dialector gorm.Dialector
|
||||||
|
|
||||||
@@ -86,14 +143,20 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
if common.EnvConfig.DbConnectionString == "" {
|
if common.EnvConfig.DbConnectionString == "" {
|
||||||
return nil, errors.New("missing required env var 'DB_CONNECTION_STRING' for SQLite database")
|
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()
|
sqliteutil.RegisterSqliteFunctions()
|
||||||
connString, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
|
|
||||||
|
connString, dbPath, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
dialector = sqlite.Open(connString)
|
||||||
case common.DbProviderPostgres:
|
case common.DbProviderPostgres:
|
||||||
if common.EnvConfig.DbConnectionString == "" {
|
if common.EnvConfig.DbConnectionString == "" {
|
||||||
@@ -123,25 +186,52 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The official C implementation of SQLite allows some additional properties in the connection string
|
func parseSqliteConnectionString(connString string) (parsedConnString string, dbPath string, err error) {
|
||||||
// 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:") {
|
if !strings.HasPrefix(connString, "file:") {
|
||||||
connString = "file:" + connString
|
connString = "file:" + connString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we're using an in-memory database
|
||||||
|
isMemoryDB := isSqliteInMemory(connString)
|
||||||
|
|
||||||
|
// Parse the connection string
|
||||||
connStringUrl, err := url.Parse(connString)
|
connStringUrl, err := url.Parse(connString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
|
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.
|
||||||
|
// Note this function updates connStringUrl.
|
||||||
|
func convertSqlitePragmaArgs(connStringUrl *url.URL) {
|
||||||
// Reference: https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string
|
// 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
|
// This only includes a subset of options, excluding those that are not relevant to us
|
||||||
qs := make(url.Values, len(connStringUrl.Query()))
|
qs := make(url.Values, len(connStringUrl.Query()))
|
||||||
for k, v := range connStringUrl.Query() {
|
for k, v := range connStringUrl.Query() {
|
||||||
switch k {
|
switch strings.ToLower(k) {
|
||||||
case "_auto_vacuum", "_vacuum":
|
case "_auto_vacuum", "_vacuum":
|
||||||
qs.Add("_pragma", "auto_vacuum("+v[0]+")")
|
qs.Add("_pragma", "auto_vacuum("+v[0]+")")
|
||||||
case "_busy_timeout", "_timeout":
|
case "_busy_timeout", "_timeout":
|
||||||
@@ -162,9 +252,165 @@ func parseSqliteConnectionString(connString string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the connStringUrl object
|
||||||
|
connStringUrl.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Get the "query string" from the connection string if present
|
||||||
|
qs := connStringUrl.Query()
|
||||||
|
if len(qs) == 0 {
|
||||||
|
qs = make(url.Values, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
connStringUrl.RawQuery = qs.Encode()
|
||||||
|
|
||||||
return connStringUrl.String(), nil
|
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 {
|
func getGormLogger() gormLogger.Interface {
|
||||||
|
|||||||
@@ -8,23 +8,93 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseSqliteConnectionString(t *testing.T) {
|
func TestIsSqliteInMemory(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
connStr string
|
||||||
expected string
|
expected bool
|
||||||
expectedError 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",
|
name: "basic file path",
|
||||||
input: "file:test.db",
|
input: "file:test.db",
|
||||||
expected: "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",
|
name: "converts _busy_timeout to pragma",
|
||||||
input: "file:test.db?_busy_timeout=5000",
|
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",
|
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",
|
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",
|
name: "basic file database",
|
||||||
input: "file:invalid#$%^&*@test.db",
|
input: "file:test.db",
|
||||||
expectedError: true,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse both URLs to compare components independently
|
|
||||||
expectedURL, err := url.Parse(tt.expected)
|
expectedURL, err := url.Parse(tt.expected)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resultURL, err := url.Parse(result)
|
compareQueryStrings(t, expectedURL, resultURL)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
|||||||
addr = common.EnvConfig.UnixSocket
|
addr = common.EnvConfig.UnixSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen(network, addr)
|
listener, err := net.Listen(network, addr) //nolint:noctx
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,22 +46,21 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client) (sv
|
|||||||
return nil, fmt.Errorf("failed to create JWT service: %w", err)
|
return nil, fmt.Errorf("failed to create JWT service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
|
|
||||||
svc.customClaimService = service.NewCustomClaimService(db)
|
svc.customClaimService = service.NewCustomClaimService(db)
|
||||||
|
|
||||||
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
|
|
||||||
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
|
|
||||||
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
|
|
||||||
|
|
||||||
svc.webauthnService, err = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
|
svc.webauthnService, err = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
|
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService)
|
||||||
|
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)
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ var oneTimeAccessTokenCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new access token that expires in 1 hour
|
// 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 {
|
if txErr != nil {
|
||||||
return fmt.Errorf("failed to generate access token: %w", txErr)
|
return fmt.Errorf("failed to generate access token: %w", txErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
DbProviderSqlite DbProvider = "sqlite"
|
DbProviderSqlite DbProvider = "sqlite"
|
||||||
DbProviderPostgres DbProvider = "postgres"
|
DbProviderPostgres DbProvider = "postgres"
|
||||||
MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz"
|
MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz"
|
||||||
defaultSqliteConnString string = "file:data/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate"
|
defaultSqliteConnString string = "data/pocket-id.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvConfigSchema struct {
|
type EnvConfigSchema struct {
|
||||||
@@ -52,6 +52,7 @@ type EnvConfigSchema struct {
|
|||||||
LogJSON bool `env:"LOG_JSON"`
|
LogJSON bool `env:"LOG_JSON"`
|
||||||
TrustProxy bool `env:"TRUST_PROXY"`
|
TrustProxy bool `env:"TRUST_PROXY"`
|
||||||
AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"`
|
AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"`
|
||||||
|
AllowDowngrade bool `env:"ALLOW_DOWNGRADE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnvConfig = defaultConfig()
|
var EnvConfig = defaultConfig()
|
||||||
@@ -87,6 +88,7 @@ func defaultConfig() EnvConfigSchema {
|
|||||||
TracingEnabled: false,
|
TracingEnabled: false,
|
||||||
TrustProxy: false,
|
TrustProxy: false,
|
||||||
AnalyticsDisabled: false,
|
AnalyticsDisabled: false,
|
||||||
|
AllowDowngrade: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,15 @@ func (e *OidcAuthorizationPendingError) HttpStatusCode() int {
|
|||||||
return http.StatusBadRequest
|
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{}
|
type OpenSignupDisabledError struct{}
|
||||||
|
|
||||||
func (e *OpenSignupDisabledError) Error() string {
|
func (e *OpenSignupDisabledError) Error() string {
|
||||||
@@ -359,3 +368,13 @@ func (e *OpenSignupDisabledError) Error() string {
|
|||||||
func (e *OpenSignupDisabledError) HttpStatusCode() int {
|
func (e *OpenSignupDisabledError) HttpStatusCode() int {
|
||||||
return http.StatusForbidden
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
|||||||
group.POST("/oidc/device/verify", authMiddleware.WithAdminNotRequired().Add(), oc.verifyDeviceCodeHandler)
|
group.POST("/oidc/device/verify", authMiddleware.WithAdminNotRequired().Add(), oc.verifyDeviceCodeHandler)
|
||||||
group.GET("/oidc/device/info", authMiddleware.WithAdminNotRequired().Add(), oc.getDeviceCodeInfoHandler)
|
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/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
|
||||||
group.GET("/oidc/users/:id/clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
|
group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
|
||||||
|
|
||||||
group.DELETE("/oidc/users/me/clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler)
|
group.DELETE("/oidc/users/me/authorized-clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler)
|
||||||
|
|
||||||
|
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAccessibleClientsHandler)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,11 +492,11 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Client ID"
|
// @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"
|
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client"
|
||||||
// @Router /api/oidc/clients/{id} [put]
|
// @Router /api/oidc/clients/{id} [put]
|
||||||
func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
||||||
var input dto.OidcClientCreateDto
|
var input dto.OidcClientUpdateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -660,7 +662,7 @@ func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
|
|||||||
// @Param sort[column] query string false "Column to sort by"
|
// @Param sort[column] query string false "Column to sort by"
|
||||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||||
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
|
// @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) {
|
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
oc.listAuthorizedClients(c, userID)
|
oc.listAuthorizedClients(c, userID)
|
||||||
@@ -676,7 +678,7 @@ func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
|
|||||||
// @Param sort[column] query string false "Column to sort by"
|
// @Param sort[column] query string false "Column to sort by"
|
||||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||||
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
|
// @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) {
|
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
|
||||||
userID := c.Param("id")
|
userID := c.Param("id")
|
||||||
oc.listAuthorizedClients(c, userID)
|
oc.listAuthorizedClients(c, userID)
|
||||||
@@ -713,7 +715,7 @@ func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
|
|||||||
// @Tags OIDC
|
// @Tags OIDC
|
||||||
// @Param clientId path string true "Client ID to revoke authorization for"
|
// @Param clientId path string true "Client ID to revoke authorization for"
|
||||||
// @Success 204 "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Router /api/oidc/users/me/clients/{clientId} [delete]
|
// @Router /api/oidc/users/me/authorized-clients/{clientId} [delete]
|
||||||
func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
|
func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
|
||||||
clientID := c.Param("clientId")
|
clientID := c.Param("clientId")
|
||||||
|
|
||||||
@@ -728,6 +730,37 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
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) {
|
func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) {
|
||||||
userCode := c.Query("code")
|
userCode := c.Query("code")
|
||||||
if userCode == "" {
|
if userCode == "" {
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import (
|
|||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultOneTimeAccessTokenDuration = 15 * time.Minute
|
||||||
|
defaultSignupTokenDuration = time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
// NewUserController creates a new controller for user management endpoints
|
// NewUserController creates a new controller for user management endpoints
|
||||||
// @Summary User management controller
|
// @Summary User management controller
|
||||||
// @Description Initializes all user-related API endpoints
|
// @Description Initializes all user-related API endpoints
|
||||||
@@ -331,10 +336,17 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ttl time.Duration
|
||||||
if own {
|
if own {
|
||||||
input.UserID = c.GetString("userID")
|
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 {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -411,7 +423,11 @@ func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context
|
|||||||
|
|
||||||
userID := c.Param("id")
|
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 {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -526,14 +542,20 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenDto dto.SignupTokenDto
|
var tokenDto dto.SignupTokenDto
|
||||||
if err := dto.MapStruct(signupToken, &tokenDto); err != nil {
|
err = dto.MapStruct(signupToken, &tokenDto)
|
||||||
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
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/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.GET("/webauthn/credentials", authMiddleware.WithAdminNotRequired().Add(), wc.listCredentialsHandler)
|
||||||
group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler)
|
group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler)
|
||||||
group.DELETE("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.deleteCredentialHandler)
|
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, "")
|
cookie.AddAccessTokenCookie(c, 0, "")
|
||||||
c.Status(http.StatusNoContent)
|
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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ type AppConfigUpdateDto struct {
|
|||||||
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
||||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||||
AllowUserSignups string `json:"allowUserSignups" binding:"required,oneof=disabled withToken open"`
|
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"`
|
AccentColor string `json:"accentColor"`
|
||||||
SmtpHost string `json:"smtpHost"`
|
SmtpHost string `json:"smtpHost"`
|
||||||
SmtpPort string `json:"smtpPort"`
|
SmtpPort string `json:"smtpPort"`
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package dto
|
|||||||
import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
|
||||||
type OidcClientMetaDataDto struct {
|
type OidcClientMetaDataDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HasLogo bool `json:"hasLogo"`
|
HasLogo bool `json:"hasLogo"`
|
||||||
LaunchURL *string `json:"launchURL"`
|
LaunchURL *string `json:"launchURL"`
|
||||||
|
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientDto struct {
|
type OidcClientDto struct {
|
||||||
@@ -28,14 +29,20 @@ type OidcClientWithAllowedGroupsCountDto struct {
|
|||||||
AllowedUserGroupsCount int64 `json:"allowedUserGroupsCount"`
|
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 {
|
type OidcClientCreateDto struct {
|
||||||
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
|
OidcClientUpdateDto
|
||||||
CallbackURLs []string `json:"callbackURLs"`
|
ID string `json:"id" binding:"omitempty,client_id,min=2,max=128"`
|
||||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
|
|
||||||
IsPublic bool `json:"isPublic"`
|
|
||||||
PkceEnabled bool `json:"pkceEnabled"`
|
|
||||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
|
||||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientCredentialsDto struct {
|
type OidcClientCredentialsDto struct {
|
||||||
@@ -50,12 +57,13 @@ type OidcClientFederatedIdentityDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeOidcClientRequestDto struct {
|
type AuthorizeOidcClientRequestDto struct {
|
||||||
ClientID string `json:"clientID" binding:"required"`
|
ClientID string `json:"clientID" binding:"required"`
|
||||||
Scope string `json:"scope" binding:"required"`
|
Scope string `json:"scope" binding:"required"`
|
||||||
CallbackURL string `json:"callbackURL"`
|
CallbackURL string `json:"callbackURL"`
|
||||||
Nonce string `json:"nonce"`
|
Nonce string `json:"nonce"`
|
||||||
CodeChallenge string `json:"codeChallenge"`
|
CodeChallenge string `json:"codeChallenge"`
|
||||||
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
||||||
|
ReauthenticationToken string `json:"reauthenticationToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeOidcClientResponseDto struct {
|
type AuthorizeOidcClientResponseDto struct {
|
||||||
@@ -159,3 +167,8 @@ type OidcClientPreviewDto struct {
|
|||||||
AccessToken map[string]any `json:"accessToken"`
|
AccessToken map[string]any `json:"accessToken"`
|
||||||
UserInfo map[string]any `json:"userInfo"`
|
UserInfo map[string]any `json:"userInfo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccessibleOidcClientDto struct {
|
||||||
|
OidcClientMetaDataDto
|
||||||
|
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SignupTokenCreateDto struct {
|
type SignupTokenCreateDto struct {
|
||||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
TTL utils.JSONDuration `json:"ttl" binding:"required,ttl"`
|
||||||
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
|
UsageLimit int `json:"usageLimit" binding:"required,min=1,max=100"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SignupTokenDto struct {
|
type SignupTokenDto struct {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserDto struct {
|
type UserDto struct {
|
||||||
@@ -30,8 +30,8 @@ type UserCreateDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OneTimeAccessTokenCreateDto struct {
|
type OneTimeAccessTokenCreateDto struct {
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
TTL utils.JSONDuration `json:"ttl" binding:"ttl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
||||||
@@ -40,7 +40,7 @@ type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OneTimeAccessEmailAsAdminDto struct {
|
type OneTimeAccessEmailAsAdminDto struct {
|
||||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
TTL utils.JSONDuration `json:"ttl" binding:"ttl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserUpdateUserGroupDto struct {
|
type UserUpdateUserGroupDto struct {
|
||||||
|
|||||||
@@ -1,29 +1,52 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/go-playground/validator/v10"
|
"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() {
|
func init() {
|
||||||
v, _ := binding.Validator.Engine().(*validator.Validate)
|
v := binding.Validator.Engine().(*validator.Validate)
|
||||||
err := v.RegisterValidation("username", validateUsername)
|
|
||||||
|
// [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 {
|
if err != nil {
|
||||||
slog.Error("Failed to register custom validation", slog.Any("error", err))
|
panic("Failed to register custom validation for username: " + err.Error())
|
||||||
os.Exit(1)
|
}
|
||||||
return
|
|
||||||
|
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, "ClearSignupTokens", def, jobs.clearSignupTokens, true),
|
||||||
s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true),
|
s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true),
|
||||||
s.registerJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, 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),
|
s.registerJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -104,6 +105,20 @@ func (j *DbCleanupJobs) clearOidcRefreshTokens(ctx context.Context) error {
|
|||||||
return nil
|
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
|
// ClearAuditLogs deletes audit logs older than 90 days
|
||||||
func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
|
func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
|
||||||
st := j.db.
|
st := j.db.
|
||||||
|
|||||||
@@ -34,13 +34,15 @@ func (a *AppConfigVariable) AsDurationMinutes() time.Duration {
|
|||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
// General
|
// General
|
||||||
AppName AppConfigVariable `key:"appName,public"` // Public
|
AppName AppConfigVariable `key:"appName,public"` // Public
|
||||||
SessionDuration AppConfigVariable `key:"sessionDuration"`
|
SessionDuration AppConfigVariable `key:"sessionDuration"`
|
||||||
EmailsVerified AppConfigVariable `key:"emailsVerified"`
|
EmailsVerified AppConfigVariable `key:"emailsVerified"`
|
||||||
AccentColor AppConfigVariable `key:"accentColor,public"` // Public
|
AccentColor AppConfigVariable `key:"accentColor,public"` // Public
|
||||||
DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public
|
DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public
|
||||||
AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
|
AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
|
||||||
AllowUserSignups AppConfigVariable `key:"allowUserSignups,public"` // Public
|
AllowUserSignups AppConfigVariable `key:"allowUserSignups,public"` // Public
|
||||||
|
SignupDefaultUserGroupIDs AppConfigVariable `key:"signupDefaultUserGroupIDs"`
|
||||||
|
SignupDefaultCustomClaims AppConfigVariable `key:"signupDefaultCustomClaims"`
|
||||||
// Internal
|
// Internal
|
||||||
BackgroundImageType AppConfigVariable `key:"backgroundImageType,internal"` // Internal
|
BackgroundImageType AppConfigVariable `key:"backgroundImageType,internal"` // Internal
|
||||||
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
||||||
|
|||||||
@@ -40,20 +40,22 @@ type OidcAuthorizationCode struct {
|
|||||||
type OidcClient struct {
|
type OidcClient struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Name string `sortable:"true"`
|
Name string `sortable:"true"`
|
||||||
Secret string
|
Secret string
|
||||||
CallbackURLs UrlList
|
CallbackURLs UrlList
|
||||||
LogoutCallbackURLs UrlList
|
LogoutCallbackURLs UrlList
|
||||||
ImageType *string
|
ImageType *string
|
||||||
HasLogo bool `gorm:"-"`
|
HasLogo bool `gorm:"-"`
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
PkceEnabled bool
|
PkceEnabled bool
|
||||||
Credentials OidcClientCredentials
|
RequiresReauthentication bool
|
||||||
LaunchURL *string
|
Credentials OidcClientCredentials
|
||||||
|
LaunchURL *string
|
||||||
|
|
||||||
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||||
CreatedByID string
|
CreatedByID *string
|
||||||
CreatedBy User
|
CreatedBy *User
|
||||||
|
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcRefreshToken struct {
|
type OidcRefreshToken struct {
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ type PublicKeyCredentialRequestOptions struct {
|
|||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReauthenticationToken struct {
|
||||||
|
Base
|
||||||
|
Token string
|
||||||
|
ExpiresAt datatype.DateTime
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
User User
|
||||||
|
}
|
||||||
|
|
||||||
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
|
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
|
||||||
|
|
||||||
// Scan and Value methods for GORM to handle the custom type
|
// Scan and Value methods for GORM to handle the custom type
|
||||||
|
|||||||
@@ -60,13 +60,15 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
|||||||
// Values are the default ones
|
// Values are the default ones
|
||||||
return &model.AppConfig{
|
return &model.AppConfig{
|
||||||
// General
|
// General
|
||||||
AppName: model.AppConfigVariable{Value: "Pocket ID"},
|
AppName: model.AppConfigVariable{Value: "Pocket ID"},
|
||||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||||
EmailsVerified: model.AppConfigVariable{Value: "false"},
|
EmailsVerified: model.AppConfigVariable{Value: "false"},
|
||||||
DisableAnimations: model.AppConfigVariable{Value: "false"},
|
DisableAnimations: model.AppConfigVariable{Value: "false"},
|
||||||
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
|
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
|
||||||
AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
|
AllowUserSignups: model.AppConfigVariable{Value: "disabled"},
|
||||||
AccentColor: model.AppConfigVariable{Value: "default"},
|
SignupDefaultUserGroupIDs: model.AppConfigVariable{Value: "[]"},
|
||||||
|
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
|
||||||
|
AccentColor: model.AppConfigVariable{Value: "default"},
|
||||||
// Internal
|
// Internal
|
||||||
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
|
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
|
||||||
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
||||||
|
|||||||
@@ -55,16 +55,46 @@ const (
|
|||||||
|
|
||||||
// UpdateCustomClaimsForUser updates the custom claims for a user
|
// UpdateCustomClaimsForUser updates the custom claims for a user
|
||||||
func (s *CustomClaimService) UpdateCustomClaimsForUser(ctx context.Context, userID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
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
|
// UpdateCustomClaimsForUserGroup updates the custom claims for a user group
|
||||||
func (s *CustomClaimService) UpdateCustomClaimsForUserGroup(ctx context.Context, userGroupID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
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
|
// updateCustomClaimsInternal updates the custom claims for a user or user group within a transaction
|
||||||
func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idType, value string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
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
|
// Check for duplicate keys in the claims slice
|
||||||
seenKeys := make(map[string]struct{})
|
seenKeys := make(map[string]struct{})
|
||||||
for _, claim := range claims {
|
for _, claim := range claims {
|
||||||
@@ -74,11 +104,6 @@ func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idTy
|
|||||||
seenKeys[claim.Key] = struct{}{}
|
seenKeys[claim.Key] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := s.db.Begin()
|
|
||||||
defer func() {
|
|
||||||
tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var existingClaims []model.CustomClaim
|
var existingClaims []model.CustomClaim
|
||||||
err := tx.
|
err := tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -150,11 +175,6 @@ func (s *CustomClaimService) updateCustomClaims(ctx context.Context, idType idTy
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit().Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedClaims, nil
|
return updatedClaims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"},
|
CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"},
|
||||||
LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"},
|
LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"},
|
||||||
ImageType: utils.StringPointer("png"),
|
ImageType: utils.StringPointer("png"),
|
||||||
CreatedByID: users[0].ID,
|
CreatedByID: utils.Ptr(users[0].ID),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
@@ -168,7 +168,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Name: "Immich",
|
Name: "Immich",
|
||||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||||
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
|
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
|
||||||
CreatedByID: users[1].ID,
|
CreatedByID: utils.Ptr(users[1].ID),
|
||||||
AllowedUserGroups: []model.UserGroup{
|
AllowedUserGroups: []model.UserGroup{
|
||||||
userGroups[1],
|
userGroups[1],
|
||||||
},
|
},
|
||||||
@@ -181,7 +181,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
|
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
|
||||||
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
|
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
|
||||||
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
|
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
|
||||||
CreatedByID: users[0].ID,
|
CreatedByID: utils.Ptr(users[0].ID),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
@@ -190,7 +190,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Name: "Federated",
|
Name: "Federated",
|
||||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||||
CallbackURLs: model.UrlList{"http://federated/auth/callback"},
|
CallbackURLs: model.UrlList{"http://federated/auth/callback"},
|
||||||
CreatedByID: users[1].ID,
|
CreatedByID: utils.Ptr(users[1].ID),
|
||||||
AllowedUserGroups: []model.UserGroup{},
|
AllowedUserGroups: []model.UserGroup{},
|
||||||
Credentials: model.OidcClientCredentials{
|
Credentials: model.OidcClientCredentials{
|
||||||
FederatedIdentities: []model.OidcClientFederatedIdentity{
|
FederatedIdentities: []model.OidcClientFederatedIdentity{
|
||||||
@@ -343,7 +343,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "b2c3d4e5-f6g7-8901-bcde-f12345678901",
|
ID: "dc3c9c96-714e-48eb-926e-2d7c7858e6cf",
|
||||||
},
|
},
|
||||||
Token: "PARTIAL567890ABC",
|
Token: "PARTIAL567890ABC",
|
||||||
ExpiresAt: datatype.DateTime(time.Now().Add(7 * 24 * time.Hour)),
|
ExpiresAt: datatype.DateTime(time.Now().Add(7 * 24 * time.Hour)),
|
||||||
@@ -352,7 +352,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "c3d4e5f6-g7h8-9012-cdef-123456789012",
|
ID: "44de1863-ffa5-4db1-9507-4887cd7a1e3f",
|
||||||
},
|
},
|
||||||
Token: "EXPIRED34567890B",
|
Token: "EXPIRED34567890B",
|
||||||
ExpiresAt: datatype.DateTime(time.Now().Add(-24 * time.Hour)), // Expired
|
ExpiresAt: datatype.DateTime(time.Now().Add(-24 * time.Hour)), // Expired
|
||||||
@@ -361,7 +361,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "d4e5f6g7-h8i9-0123-def0-234567890123",
|
ID: "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b",
|
||||||
},
|
},
|
||||||
Token: "FULLYUSED567890C",
|
Token: "FULLYUSED567890C",
|
||||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type OidcService struct {
|
|||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
auditLogService *AuditLogService
|
auditLogService *AuditLogService
|
||||||
customClaimService *CustomClaimService
|
customClaimService *CustomClaimService
|
||||||
|
webAuthnService *WebAuthnService
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
jwkCache *jwk.Cache
|
jwkCache *jwk.Cache
|
||||||
@@ -62,6 +63,7 @@ func NewOidcService(
|
|||||||
appConfigService *AppConfigService,
|
appConfigService *AppConfigService,
|
||||||
auditLogService *AuditLogService,
|
auditLogService *AuditLogService,
|
||||||
customClaimService *CustomClaimService,
|
customClaimService *CustomClaimService,
|
||||||
|
webAuthnService *WebAuthnService,
|
||||||
) (s *OidcService, err error) {
|
) (s *OidcService, err error) {
|
||||||
s = &OidcService{
|
s = &OidcService{
|
||||||
db: db,
|
db: db,
|
||||||
@@ -69,6 +71,7 @@ func NewOidcService(
|
|||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
auditLogService: auditLogService,
|
auditLogService: auditLogService,
|
||||||
customClaimService: customClaimService,
|
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
|
// 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
|
||||||
@@ -123,6 +126,16 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
|||||||
return "", "", err
|
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 the client is not public, the code challenge must be provided
|
||||||
if client.IsPublic && input.CodeChallenge == "" {
|
if client.IsPublic && input.CodeChallenge == "" {
|
||||||
return "", "", &common.OidcMissingCodeChallengeError{}
|
return "", "", &common.OidcMissingCodeChallengeError{}
|
||||||
@@ -641,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
|
// 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" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||||
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && isValidSortDirection {
|
|
||||||
query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
|
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").
|
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
||||||
Group("oidc_clients.id").
|
Group("oidc_clients.id").
|
||||||
@@ -658,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) {
|
func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) {
|
||||||
client := model.OidcClient{
|
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.
|
err := s.db.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Create(&client).
|
Create(&client).
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return model.OidcClient{}, &common.ClientIdAlreadyExistsError{}
|
||||||
|
}
|
||||||
return model.OidcClient{}, err
|
return model.OidcClient{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
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()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -707,7 +725,7 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientCreateDto) {
|
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
|
||||||
// Base fields
|
// Base fields
|
||||||
client.Name = input.Name
|
client.Name = input.Name
|
||||||
client.CallbackURLs = input.CallbackURLs
|
client.CallbackURLs = input.CallbackURLs
|
||||||
@@ -715,20 +733,20 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
|
|||||||
client.IsPublic = input.IsPublic
|
client.IsPublic = input.IsPublic
|
||||||
// PKCE is required for public clients
|
// PKCE is required for public clients
|
||||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||||
|
client.RequiresReauthentication = input.RequiresReauthentication
|
||||||
client.LaunchURL = input.LaunchURL
|
client.LaunchURL = input.LaunchURL
|
||||||
|
|
||||||
// Credentials
|
// Credentials
|
||||||
if len(input.Credentials.FederatedIdentities) > 0 {
|
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
||||||
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
for i, fi := range input.Credentials.FederatedIdentities {
|
||||||
for i, fi := range input.Credentials.FederatedIdentities {
|
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
|
||||||
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
|
Issuer: fi.Issuer,
|
||||||
Issuer: fi.Issuer,
|
Audience: fi.Audience,
|
||||||
Audience: fi.Audience,
|
Subject: fi.Subject,
|
||||||
Subject: fi.Subject,
|
JWKS: fi.JWKS,
|
||||||
JWKS: fi.JWKS,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error {
|
func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error {
|
||||||
@@ -1336,6 +1354,80 @@ func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string,
|
|||||||
return nil
|
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) {
|
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, tx *gorm.DB) (string, error) {
|
||||||
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
|
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1462,8 +1554,8 @@ func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *g
|
|||||||
|
|
||||||
// Validate credentials based on the authentication method
|
// Validate credentials based on the authentication method
|
||||||
switch {
|
switch {
|
||||||
// First, if we have a client secret, we validate it
|
// First, if we have a client secret, we validate it unless client is marked as public
|
||||||
case input.ClientSecret != "":
|
case input.ClientSecret != "" && !client.IsPublic:
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(input.ClientSecret))
|
err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(input.ClientSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &common.OidcClientSecretInvalidError{}
|
return nil, &common.OidcClientSecretInvalidError{}
|
||||||
|
|||||||
@@ -171,8 +171,10 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
|||||||
// Create the test clients
|
// Create the test clients
|
||||||
// 1. Confidential client
|
// 1. Confidential client
|
||||||
confidentialClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
confidentialClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||||
Name: "Confidential Client",
|
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||||
CallbackURLs: []string{"https://example.com/callback"},
|
Name: "Confidential Client",
|
||||||
|
CallbackURLs: []string{"https://example.com/callback"},
|
||||||
|
},
|
||||||
}, "test-user-id")
|
}, "test-user-id")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -182,20 +184,24 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
|||||||
|
|
||||||
// 2. Public client
|
// 2. Public client
|
||||||
publicClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
publicClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||||
Name: "Public Client",
|
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||||
CallbackURLs: []string{"https://example.com/callback"},
|
Name: "Public Client",
|
||||||
IsPublic: true,
|
CallbackURLs: []string{"https://example.com/callback"},
|
||||||
|
IsPublic: true,
|
||||||
|
},
|
||||||
}, "test-user-id")
|
}, "test-user-id")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// 3. Confidential client with federated identity
|
// 3. Confidential client with federated identity
|
||||||
federatedClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
federatedClient, err := s.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||||
Name: "Federated Client",
|
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||||
CallbackURLs: []string{"https://example.com/callback"},
|
Name: "Federated Client",
|
||||||
|
CallbackURLs: []string{"https://example.com/callback"},
|
||||||
|
},
|
||||||
}, "test-user-id")
|
}, "test-user-id")
|
||||||
require.NoError(t, err)
|
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,
|
Name: federatedClient.Name,
|
||||||
CallbackURLs: federatedClient.CallbackURLs,
|
CallbackURLs: federatedClient.CallbackURLs,
|
||||||
Credentials: dto.OidcClientCredentialsDto{
|
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
|
// 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" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
||||||
if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection {
|
|
||||||
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
|
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").
|
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
|
||||||
Group("user_groups.id").
|
Group("user_groups.id").
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -26,20 +27,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
jwtService *JwtService
|
jwtService *JwtService
|
||||||
auditLogService *AuditLogService
|
auditLogService *AuditLogService
|
||||||
emailService *EmailService
|
emailService *EmailService
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
|
customClaimService *CustomClaimService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService) *UserService {
|
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService) *UserService {
|
||||||
return &UserService{
|
return &UserService{
|
||||||
db: db,
|
db: db,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
auditLogService: auditLogService,
|
auditLogService: auditLogService,
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
|
customClaimService: customClaimService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +271,53 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
|
|||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return model.User{}, err
|
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
|
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) {
|
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool) (model.User, error) {
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -348,13 +395,13 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
|||||||
return user, nil
|
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()
|
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsAdminEnabled.IsTrue()
|
||||||
if isDisabled {
|
if isDisabled {
|
||||||
return &common.OneTimeAccessDisabledError{}
|
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 {
|
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error {
|
||||||
@@ -374,11 +421,10 @@ func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expiration := time.Now().Add(15 * time.Minute)
|
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute)
|
||||||
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, expiration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -389,7 +435,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, expiration, tx)
|
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -421,7 +467,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
|||||||
Code: oneTimeAccessToken,
|
Code: oneTimeAccessToken,
|
||||||
LoginLink: link,
|
LoginLink: link,
|
||||||
LoginLinkWithCode: linkWithCode,
|
LoginLinkWithCode: linkWithCode,
|
||||||
ExpirationString: utils.DurationToString(time.Until(expiration).Round(time.Second)),
|
ExpirationString: utils.DurationToString(ttl),
|
||||||
})
|
})
|
||||||
if errInternal != nil {
|
if errInternal != nil {
|
||||||
slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email))
|
slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email))
|
||||||
@@ -432,17 +478,18 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, expiresAt time.Time) (string, error) {
|
func (s *UserService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error) {
|
||||||
return s.createOneTimeAccessTokenInternal(ctx, userID, expiresAt, s.db)
|
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) {
|
func (s *UserService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, ttl time.Duration, tx *gorm.DB) (string, error) {
|
||||||
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, expiresAt)
|
oneTimeAccessToken, err := NewOneTimeAccessToken(userID, ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.WithContext(ctx).Create(oneTimeAccessToken).Error; err != nil {
|
err = tx.WithContext(ctx).Create(oneTimeAccessToken).Error
|
||||||
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +551,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
|
|||||||
// Fetch the groups based on userGroupIds
|
// Fetch the groups based on userGroupIds
|
||||||
var groups []model.UserGroup
|
var groups []model.UserGroup
|
||||||
if len(userGroupIds) > 0 {
|
if len(userGroupIds) > 0 {
|
||||||
err = tx.
|
err := tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Where("id IN (?)", userGroupIds).
|
Where("id IN (?)", userGroupIds).
|
||||||
Find(&groups).
|
Find(&groups).
|
||||||
@@ -642,17 +689,14 @@ func (s *UserService) disableUserInternal(ctx context.Context, userID string, tx
|
|||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CreateSignupToken(ctx context.Context, expiresAt time.Time, usageLimit int) (model.SignupToken, error) {
|
func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int) (model.SignupToken, error) {
|
||||||
return s.createSignupTokenInternal(ctx, expiresAt, usageLimit, s.db)
|
signupToken, err := NewSignupToken(ttl, usageLimit)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UserService) createSignupTokenInternal(ctx context.Context, expiresAt time.Time, usageLimit int, tx *gorm.DB) (model.SignupToken, error) {
|
|
||||||
signupToken, err := NewSignupToken(expiresAt, usageLimit)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.SignupToken{}, err
|
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
|
return model.SignupToken{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,10 +790,10 @@ func (s *UserService) DeleteSignupToken(ctx context.Context, tokenID string) err
|
|||||||
return s.db.WithContext(ctx).Delete(&model.SignupToken{}, "id = ?", tokenID).Error
|
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
|
// If expires at is less than 15 minutes, use a 6-character token instead of 16
|
||||||
tokenLength := 16
|
tokenLength := 16
|
||||||
if time.Until(expiresAt) <= 15*time.Minute {
|
if ttl <= 15*time.Minute {
|
||||||
tokenLength = 6
|
tokenLength = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,25 +802,27 @@ func NewOneTimeAccessToken(userID string, expiresAt time.Time) (*model.OneTimeAc
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now().Round(time.Second)
|
||||||
o := &model.OneTimeAccessToken{
|
o := &model.OneTimeAccessToken{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ExpiresAt: datatype.DateTime(expiresAt),
|
ExpiresAt: datatype.DateTime(now.Add(ttl)),
|
||||||
Token: randomString,
|
Token: randomString,
|
||||||
}
|
}
|
||||||
|
|
||||||
return o, nil
|
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
|
// Generate a random token
|
||||||
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now().Round(time.Second)
|
||||||
token := &model.SignupToken{
|
token := &model.SignupToken{
|
||||||
Token: randomString,
|
Token: randomString,
|
||||||
ExpiresAt: datatype.DateTime(expiresAt),
|
ExpiresAt: datatype.DateTime(now.Add(ttl)),
|
||||||
UsageLimit: usageLimit,
|
UsageLimit: usageLimit,
|
||||||
UsageCount: 0,
|
UsageCount: 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,13 +221,15 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
|
|||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Load & delete the session row
|
||||||
var storedSession model.WebauthnSession
|
var storedSession model.WebauthnSession
|
||||||
err := tx.
|
err := tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
First(&storedSession, "id = ?", sessionID).
|
Clauses(clause.Returning{}).
|
||||||
|
Delete(&storedSession, "id = ?", sessionID).
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.User{}, "", err
|
return model.User{}, "", fmt.Errorf("failed to load WebAuthn session: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
session := webauthn.SessionData{
|
session := webauthn.SessionData{
|
||||||
@@ -334,3 +336,136 @@ func (s *WebAuthnService) UpdateCredential(ctx context.Context, userID, credenti
|
|||||||
func (s *WebAuthnService) updateWebAuthnConfig() {
|
func (s *WebAuthnService) updateWebAuthnConfig() {
|
||||||
s.webAuthn.Config.RPDisplayName = s.appConfigService.GetDbConfig().AppName.Value
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pocket-id/pocket-id/backend/resources"
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
@@ -136,3 +139,41 @@ func FileExists(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
return !s.IsDir(), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@@ -35,9 +36,7 @@ func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gor
|
|||||||
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
|
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
|
||||||
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
|
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
|
||||||
|
|
||||||
if sort.Direction == "" || (sort.Direction != "asc" && sort.Direction != "desc") {
|
sort.Direction = NormalizeSortDirection(sort.Direction)
|
||||||
sort.Direction = "asc"
|
|
||||||
}
|
|
||||||
|
|
||||||
if sortFieldFound && isSortable {
|
if sortFieldFound && isSortable {
|
||||||
columnName := CamelCaseToSnakeCase(sort.Column)
|
columnName := CamelCaseToSnakeCase(sort.Column)
|
||||||
@@ -85,3 +84,16 @@ func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (Pagin
|
|||||||
ItemsPerPage: pageSize,
|
ItemsPerPage: pageSize,
|
||||||
}, nil
|
}, 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"
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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 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",
|
"click_to_copy": "Kliknutím zkopírujete",
|
||||||
"something_went_wrong": "Něco se pokazilo",
|
"something_went_wrong": "Něco se pokazilo",
|
||||||
"go_back_to_home": "Přejít zpět domů",
|
"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",
|
"login_background": "Pozadí přihlašovací stránky",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Přihlašovací kód",
|
"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.",
|
"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",
|
"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ů.",
|
"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}",
|
"name_logo": "Logo {name}",
|
||||||
"change_logo": "Změnit logo",
|
"change_logo": "Změnit logo",
|
||||||
"upload_logo": "Nahrát 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.",
|
"number_of_times_token_can_be_used": "Kolikrát lze použít registrační token.",
|
||||||
"expires": "Vyprší",
|
"expires": "Vyprší",
|
||||||
"signup": "Zaregistrovat se",
|
"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",
|
"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",
|
"validating_signup_token": "Ověřování registračního tokenu",
|
||||||
"go_to_login": "Přejít na přihlášení",
|
"go_to_login": "Přejít na přihlášení",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Prozatím přeskočit",
|
"skip_for_now": "Prozatím přeskočit",
|
||||||
"account_created": "Účet vytvořen",
|
"account_created": "Účet vytvořen",
|
||||||
"enable_user_signups": "Povolit registraci uživatelů",
|
"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",
|
"user_signups_are_disabled": "Registrace uživatelů jsou v současné době zakázány",
|
||||||
"create_signup_token": "Vytvořit registrační token",
|
"create_signup_token": "Vytvořit registrační token",
|
||||||
"view_active_signup_tokens": "Zobrazit aktivní registrační tokeny",
|
"view_active_signup_tokens": "Zobrazit aktivní registrační tokeny",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Načítání",
|
"loading": "Načítání",
|
||||||
"delete_signup_token": "Odstranit registrační token",
|
"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.",
|
"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": "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_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": "Otevřená registrace",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Název klienta, který se zobrazuje v uživatelském rozhraní Pocket ID.",
|
"client_name_description": "Název klienta, který se zobrazuje v uživatelském rozhraní Pocket ID.",
|
||||||
"revoke_access": "Zrušit přístup",
|
"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_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."
|
"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",
|
"click_to_copy": "Klik for at kopiere",
|
||||||
"something_went_wrong": "Noget gik galt",
|
"something_went_wrong": "Noget gik galt",
|
||||||
"go_back_to_home": "Gå tilbage til hjem",
|
"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",
|
"login_background": "Log ind baggrund",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Loginkode",
|
"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.",
|
"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",
|
"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.",
|
"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}",
|
"name_logo": "Logo for {name}",
|
||||||
"change_logo": "Skift logo",
|
"change_logo": "Skift logo",
|
||||||
"upload_logo": "Upload logo",
|
"upload_logo": "Upload logo",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "Antal gange, som tilmeldingstokenet kan bruges.",
|
"number_of_times_token_can_be_used": "Antal gange, som tilmeldingstokenet kan bruges.",
|
||||||
"expires": "Udløber",
|
"expires": "Udløber",
|
||||||
"signup": "Tilmeld",
|
"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.",
|
"signup_requires_valid_token": "Der kræves en gyldig tilmeldingstoken for at oprette en konto.",
|
||||||
"validating_signup_token": "Validering af tilmeldingstoken",
|
"validating_signup_token": "Validering af tilmeldingstoken",
|
||||||
"go_to_login": "Gå til login",
|
"go_to_login": "Gå til login",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Spring over for nu",
|
"skip_for_now": "Spring over for nu",
|
||||||
"account_created": "Konto oprettet",
|
"account_created": "Konto oprettet",
|
||||||
"enable_user_signups": "Aktiver brugerregistrering",
|
"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.",
|
"user_signups_are_disabled": "Brugerregistrering er i øjeblikket deaktiveret.",
|
||||||
"create_signup_token": "Opret tilmeldingstoken",
|
"create_signup_token": "Opret tilmeldingstoken",
|
||||||
"view_active_signup_tokens": "Vis aktive tilmeldingstokener",
|
"view_active_signup_tokens": "Vis aktive tilmeldingstokener",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Indlæsning",
|
"loading": "Indlæsning",
|
||||||
"delete_signup_token": "Slet tilmeldingstoken",
|
"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.",
|
"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": "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_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": "Åben tilmelding",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.",
|
"client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.",
|
||||||
"revoke_access": "Tilbagekald adgang",
|
"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_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."
|
"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",
|
"click_to_copy": "Zum Kopieren klicken",
|
||||||
"something_went_wrong": "Etwas ist schiefgelaufen",
|
"something_went_wrong": "Etwas ist schiefgelaufen",
|
||||||
"go_back_to_home": "Zurück zur Startseite",
|
"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",
|
"login_background": "Login Hintergrund",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Anmeldecode",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"name_logo": "{name} Logo",
|
||||||
"change_logo": "Logo ändern",
|
"change_logo": "Logo ändern",
|
||||||
"upload_logo": "Logo hochladen",
|
"upload_logo": "Logo hochladen",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "Wie oft der Anmeldetoken benutzt werden kann.",
|
"number_of_times_token_can_be_used": "Wie oft der Anmeldetoken benutzt werden kann.",
|
||||||
"expires": "Läuft ab",
|
"expires": "Läuft ab",
|
||||||
"signup": "Anmelden",
|
"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.",
|
"signup_requires_valid_token": "Zum Erstellen eines Kontos brauchst du einen gültigen Anmeldetoken.",
|
||||||
"validating_signup_token": "Anmeldungstoken bestätigen",
|
"validating_signup_token": "Anmeldungstoken bestätigen",
|
||||||
"go_to_login": "Zum Login gehen",
|
"go_to_login": "Zum Login gehen",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Jetzt überspringen",
|
"skip_for_now": "Jetzt überspringen",
|
||||||
"account_created": "Konto erstellt",
|
"account_created": "Konto erstellt",
|
||||||
"enable_user_signups": "Benutzeranmeldungen aktivieren",
|
"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.",
|
"user_signups_are_disabled": "Benutzeranmeldungen sind im Moment deaktiviert.",
|
||||||
"create_signup_token": "Anmeldungstoken erstellen",
|
"create_signup_token": "Anmeldungstoken erstellen",
|
||||||
"view_active_signup_tokens": "Aktive Anmeldetoken anzeigen",
|
"view_active_signup_tokens": "Aktive Anmeldetoken anzeigen",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Laden",
|
"loading": "Laden",
|
||||||
"delete_signup_token": "Anmeldungstoken löschen",
|
"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.",
|
"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": "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_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": "Anmeldung offen",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Der Name des Clients, der in der Pocket ID-Benutzeroberfläche angezeigt wird.",
|
"client_name_description": "Der Name des Clients, der in der Pocket ID-Benutzeroberfläche angezeigt wird.",
|
||||||
"revoke_access": "Zugriff widerrufen",
|
"revoke_access": "Zugriff widerrufen",
|
||||||
"revoke_access_description": "Zugriff widerrufen <b>{clientName}</b>. <b>{clientName}</b> kann nicht mehr auf deine Kontoinfos zugreifen.",
|
"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."
|
"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",
|
"click_to_copy": "Click to copy",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
"go_back_to_home": "Go back to home",
|
"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",
|
"login_background": "Login background",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Login Code",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"name_logo": "{name} logo",
|
||||||
"change_logo": "Change Logo",
|
"change_logo": "Change Logo",
|
||||||
"upload_logo": "Upload 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.",
|
"number_of_times_token_can_be_used": "Number of times the signup token can be used.",
|
||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"signup": "Sign Up",
|
"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",
|
"signup_requires_valid_token": "A valid signup token is required to create an account",
|
||||||
"validating_signup_token": "Validating signup token",
|
"validating_signup_token": "Validating signup token",
|
||||||
"go_to_login": "Go to login",
|
"go_to_login": "Go to login",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Skip for now",
|
"skip_for_now": "Skip for now",
|
||||||
"account_created": "Account Created",
|
"account_created": "Account Created",
|
||||||
"enable_user_signups": "Enable User Signups",
|
"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",
|
"user_signups_are_disabled": "User signups are currently disabled",
|
||||||
"create_signup_token": "Create Signup Token",
|
"create_signup_token": "Create Signup Token",
|
||||||
"view_active_signup_tokens": "View Active Signup Tokens",
|
"view_active_signup_tokens": "View Active Signup Tokens",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"delete_signup_token": "Delete Signup Token",
|
"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.",
|
"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": "Signup with token",
|
||||||
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
|
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
|
||||||
"signup_open": "Open Signup",
|
"signup_open": "Open Signup",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
|
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
|
||||||
"revoke_access": "Revoke Access",
|
"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_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."
|
"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",
|
"click_to_copy": "Haz clic para copiar",
|
||||||
"something_went_wrong": "Algo ha salido mal",
|
"something_went_wrong": "Algo ha salido mal",
|
||||||
"go_back_to_home": "Volver al Inicio",
|
"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",
|
"login_background": "Fondo de página de acceso",
|
||||||
"logo": "Logotipo",
|
"logo": "Logotipo",
|
||||||
"login_code": "Código de inicio de sesión",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"name_logo": "{name} logotipo",
|
||||||
"change_logo": "Cambiar logotipo",
|
"change_logo": "Cambiar logotipo",
|
||||||
"upload_logo": "Subir Logo",
|
"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.",
|
"number_of_times_token_can_be_used": "Número de veces que se puede utilizar el token de registro.",
|
||||||
"expires": "Caduca",
|
"expires": "Caduca",
|
||||||
"signup": "Regístrate",
|
"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.",
|
"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",
|
"validating_signup_token": "Validación del token de registro",
|
||||||
"go_to_login": "Ir al inicio de sesión",
|
"go_to_login": "Ir al inicio de sesión",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Cargando",
|
"loading": "Cargando",
|
||||||
"delete_signup_token": "Eliminar token de registro",
|
"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.",
|
"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": "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_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": "Inscripción abierta",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "El nombre del cliente que aparece en la interfaz de usuario de Pocket ID.",
|
"client_name_description": "El nombre del cliente que aparece en la interfaz de usuario de Pocket ID.",
|
||||||
"revoke_access": "Revocar acceso",
|
"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_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."
|
"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",
|
"click_to_copy": "Cliquer pour copier",
|
||||||
"something_went_wrong": "Quelque chose n'a pas fonctionné",
|
"something_went_wrong": "Quelque chose n'a pas fonctionné",
|
||||||
"go_back_to_home": "Retourner à l'accueil",
|
"go_back_to_home": "Retourner à l'accueil",
|
||||||
"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",
|
"login_background": "Arrière-plan de connexion",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Code de connexion",
|
"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.",
|
"public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
|
||||||
"pkce": "PKCE",
|
"pkce": "PKCE",
|
||||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et l’interception de code d’autorisation.",
|
"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}",
|
"name_logo": "Logo {name}",
|
||||||
"change_logo": "Changer le logo",
|
"change_logo": "Changer le logo",
|
||||||
"upload_logo": "Télécharger un 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é.",
|
"number_of_times_token_can_be_used": "Nombre de fois que le jeton d'inscription peut être utilisé.",
|
||||||
"expires": "Expire",
|
"expires": "Expire",
|
||||||
"signup": "S'inscrire",
|
"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.",
|
"signup_requires_valid_token": "Un jeton d'inscription valide est requis pour créer un compte.",
|
||||||
"validating_signup_token": "Validation du jeton d'inscription",
|
"validating_signup_token": "Validation du jeton d'inscription",
|
||||||
"go_to_login": "Aller à la connexion",
|
"go_to_login": "Aller à la connexion",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Ignorer pour le moment",
|
"skip_for_now": "Ignorer pour le moment",
|
||||||
"account_created": "Compte créé",
|
"account_created": "Compte créé",
|
||||||
"enable_user_signups": "Activer les inscriptions utilisateur",
|
"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",
|
"user_signups_are_disabled": "Les inscriptions utilisateur sont actuellement désactivées",
|
||||||
"create_signup_token": "Créer un jeton d'inscription",
|
"create_signup_token": "Créer un jeton d'inscription",
|
||||||
"view_active_signup_tokens": "Voir les jetons d'inscription actifs",
|
"view_active_signup_tokens": "Voir les jetons d'inscription actifs",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Chargement",
|
"loading": "Chargement",
|
||||||
"delete_signup_token": "Supprimer le jeton d'inscription",
|
"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.",
|
"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": "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_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": "Inscription ouverte",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
|
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
|
||||||
"revoke_access": "Supprimer l'accès",
|
"revoke_access": "Supprimer l'accès",
|
||||||
"revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de ton compte.",
|
"revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de ton compte.",
|
||||||
"revoke_access_successful": "L'accès à {clientName} a été supprimé."
|
"revoke_access_successful": "L'accès à {clientName} a été supprimé.",
|
||||||
|
"last_signed_in_ago": "Dernière connexion il y a {time} il y a",
|
||||||
|
"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",
|
"click_to_copy": "Clicca per copiare",
|
||||||
"something_went_wrong": "Qualcosa è andato storto",
|
"something_went_wrong": "Qualcosa è andato storto",
|
||||||
"go_back_to_home": "Torna alla home",
|
"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",
|
"login_background": "Sfondo di accesso",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Codice di accesso",
|
"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.",
|
"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",
|
"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.",
|
"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}",
|
"name_logo": "Logo di {name}",
|
||||||
"change_logo": "Cambia Logo",
|
"change_logo": "Cambia Logo",
|
||||||
"upload_logo": "Carica 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.",
|
"number_of_times_token_can_be_used": "Numero di volte che il codice d'iscrizione può essere usato.",
|
||||||
"expires": "Scadenza",
|
"expires": "Scadenza",
|
||||||
"signup": "Registrati",
|
"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",
|
"signup_requires_valid_token": "È necessario un codice d'iscrizione valido per creare un account",
|
||||||
"validating_signup_token": "Convalida codice d'iscrizione",
|
"validating_signup_token": "Convalida codice d'iscrizione",
|
||||||
"go_to_login": "Vai alla login",
|
"go_to_login": "Vai alla login",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Salta per ora",
|
"skip_for_now": "Salta per ora",
|
||||||
"account_created": "Account creato",
|
"account_created": "Account creato",
|
||||||
"enable_user_signups": "Abilita Iscrizioni Utente",
|
"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",
|
"user_signups_are_disabled": "Le iscrizioni utente sono attualmente disattivate",
|
||||||
"create_signup_token": "Crea Codice d'iscrizione",
|
"create_signup_token": "Crea Codice d'iscrizione",
|
||||||
"view_active_signup_tokens": "Visualizza codici d'iscrizione attivi",
|
"view_active_signup_tokens": "Visualizza codici d'iscrizione attivi",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Caricamento",
|
"loading": "Caricamento",
|
||||||
"delete_signup_token": "Elimina Codice d'iscrizione",
|
"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.",
|
"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": "Registrati con codice",
|
||||||
"signup_with_token_description": "Gli utenti possono registrarsi solo usando un codice d'iscrizione valido, creato da un amministratore.",
|
"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": "Apri Registrazione",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Il nome del cliente che appare nell'interfaccia utente Pocket ID.",
|
"client_name_description": "Il nome del cliente che appare nell'interfaccia utente Pocket ID.",
|
||||||
"revoke_access": "Revoca accesso",
|
"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_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."
|
"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",
|
"my_account": "Mijn account",
|
||||||
"logout": "Uitloggen",
|
"logout": "Uitloggen",
|
||||||
"confirm": "Bevestigen",
|
"confirm": "Bevestigen",
|
||||||
"docs": "Documenten",
|
"docs": "Documentatie",
|
||||||
"key": "Sleutel",
|
"key": "Sleutel",
|
||||||
"value": "Waarde",
|
"value": "Waarde",
|
||||||
"remove_custom_claim": "Aangepaste claim verwijderen",
|
"remove_custom_claim": "Aangepaste claim verwijderen",
|
||||||
"add_custom_claim": "Aangepaste claim toevoegen",
|
"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_a_date": "Selecteer een datum",
|
||||||
"select_file": "Selecteer bestand",
|
"select_file": "Selecteer bestand",
|
||||||
"profile_picture": "Profielfoto",
|
"profile_picture": "Profielfoto",
|
||||||
"profile_picture_is_managed_by_ldap_server": "De profielfoto wordt beheerd door de LDAP-server en kan hier niet worden gewijzigd.",
|
"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.",
|
"image_should_be_in_format": "De afbeelding moet in PNG- of JPEG-formaat zijn.",
|
||||||
"items_per_page": "Aantal per pagina",
|
"items_per_page": "Aantal per pagina",
|
||||||
"no_items_found": "Geen items gevonden",
|
"no_items_found": "Geen items gevonden",
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
"copied": "Gekopieerd",
|
"copied": "Gekopieerd",
|
||||||
"click_to_copy": "Klik om te kopiëren",
|
"click_to_copy": "Klik om te kopiëren",
|
||||||
"something_went_wrong": "Er is iets misgegaan",
|
"something_went_wrong": "Er is iets misgegaan",
|
||||||
"go_back_to_home": "Ga terug naar huis",
|
"go_back_to_home": "Terug naar beginpagina",
|
||||||
"dont_have_access_to_your_passkey": "Heeft u geen toegang tot uw toegangscode?",
|
"alternative_sign_in_methods": "Andere manieren om in te loggen",
|
||||||
"login_background": "Inlogachtergrond",
|
"login_background": "Inlogachtergrond",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Inlogcode",
|
"login_code": "Inlogcode",
|
||||||
@@ -42,49 +42,49 @@
|
|||||||
"authentication_process_was_aborted": "Het authenticatieproces is afgebroken",
|
"authentication_process_was_aborted": "Het authenticatieproces is afgebroken",
|
||||||
"error_occurred_with_authenticator": "Er is een fout opgetreden met de authenticator",
|
"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_discoverable_credentials": "De authenticator ondersteunt geen vindbare referenties",
|
||||||
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen residente sleutels",
|
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen vaste sleutels",
|
||||||
"passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd",
|
"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_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen",
|
||||||
"authenticator_timed_out": "De authenticator is verlopen",
|
"authenticator_timed_out": "De authenticator is verlopen",
|
||||||
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met uw beheerder.",
|
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met de beheerder.",
|
||||||
"sign_in_to": "Meld u aan bij {name}",
|
"sign_in_to": "Meld je aan bij {name}",
|
||||||
"client_not_found": "Client niet gevonden",
|
"client_not_found": "Client niet gevonden",
|
||||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wil toegang tot de volgende informatie:",
|
"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",
|
"email": "E-mail",
|
||||||
"view_your_email_address": "Bekijk uw e-mailadres",
|
"view_your_email_address": "Bekijk je e-mailadres",
|
||||||
"profile": "Profiel",
|
"profile": "Profiel",
|
||||||
"view_your_profile_information": "Bekijk uw profielgegevens",
|
"view_your_profile_information": "Bekijk je profielgegevens",
|
||||||
"groups": "Groepen",
|
"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",
|
"cancel": "Annuleren",
|
||||||
"sign_in": "Aanmelden",
|
"sign_in": "Aanmelden",
|
||||||
"try_again": "Probeer het opnieuw",
|
"try_again": "Probeer het opnieuw",
|
||||||
"client_logo": "Client logo",
|
"client_logo": "Client logo",
|
||||||
"sign_out": "Afmelden",
|
"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> ?",
|
"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 u aan bij {appName}",
|
"sign_in_to_appname": "Meld je aan bij {appName}",
|
||||||
"please_try_to_sign_in_again": "Probeer opnieuw in te loggen.",
|
"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",
|
"authenticate": "Authenticeren",
|
||||||
"please_try_again": "Probeer het opnieuw.",
|
"please_try_again": "Probeer het opnieuw.",
|
||||||
"continue": "Doorgaan",
|
"continue": "Doorgaan",
|
||||||
"alternative_sign_in": "Alternatieve aanmelding",
|
"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.",
|
"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": "Wilt u in plaats daarvan uw toegangscode gebruiken?",
|
"use_your_passkey_instead": "Wil je in plaats daarvan je passkey gebruiken?",
|
||||||
"email_login": "E-mail inloggen",
|
"email_login": "E-mail inloggen",
|
||||||
"enter_a_login_code_to_sign_in": "Voer een inlogcode in om in te loggen.",
|
"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.",
|
"request_a_login_code_via_email": "Vraag een inlogcode aan via e-mail.",
|
||||||
"go_back": "Ga terug",
|
"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 dit in het systeem voorkomt.",
|
"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_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.",
|
"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": "Uw e-mail",
|
"your_email": "Je e-mail",
|
||||||
"submit": "Indienen",
|
"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",
|
"code": "Code",
|
||||||
"invalid_redirect_url": "Ongeldige omleidings-URL",
|
"invalid_redirect_url": "Ongeldige omleidings-URL",
|
||||||
"audit_log": "Audit logboek",
|
"audit_log": "Activiteitenlogboek",
|
||||||
"users": "Gebruikers",
|
"users": "Gebruikers",
|
||||||
"user_groups": "Gebruikersgroepen",
|
"user_groups": "Gebruikersgroepen",
|
||||||
"oidc_clients": "OIDC-clients",
|
"oidc_clients": "OIDC-clients",
|
||||||
@@ -93,52 +93,52 @@
|
|||||||
"settings": "Instellingen",
|
"settings": "Instellingen",
|
||||||
"update_pocket_id": "Pocket-ID bijwerken",
|
"update_pocket_id": "Pocket-ID bijwerken",
|
||||||
"powered_by": "Aangedreven door",
|
"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",
|
"time": "Tijd",
|
||||||
"event": "Evenement",
|
"event": "Activiteit",
|
||||||
"approximate_location": "Geschatte locatie",
|
"approximate_location": "Geschatte locatie",
|
||||||
"ip_address": "IP-adres",
|
"ip_address": "IP-adres",
|
||||||
"device": "Apparaat",
|
"device": "Apparaat",
|
||||||
"client": "Cliënt",
|
"client": "Client",
|
||||||
"unknown": "Onbekend",
|
"unknown": "Onbekend",
|
||||||
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
|
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
|
||||||
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||||
"account_settings": "Accountinstellingen",
|
"account_settings": "Accountinstellingen",
|
||||||
"passkey_missing": "Passkey ontbreekt",
|
"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.",
|
"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 toegangscode geconfigureerd",
|
"single_passkey_configured": "Eén enkele passkey 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.",
|
"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",
|
"account_details": "Accountgegevens",
|
||||||
"passkeys": "Toegangscodes",
|
"passkeys": "Passkeys",
|
||||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.",
|
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de passkeys waarmee je jezelf kunt verifiëren.",
|
||||||
"add_passkey": "Passkey toevoegen",
|
"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_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",
|
"first_name": "Voornaam",
|
||||||
"last_name": "Achternaam",
|
"last_name": "Achternaam",
|
||||||
"username": "Gebruikersnaam",
|
"username": "Gebruikersnaam",
|
||||||
"save": "Opslaan",
|
"save": "Opslaan",
|
||||||
"username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten",
|
"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",
|
"or_visit": "of bezoek",
|
||||||
"added_on": "Toegevoegd op",
|
"added_on": "Toegevoegd op",
|
||||||
"rename": "Hernoemen",
|
"rename": "Hernoemen",
|
||||||
"delete": "Verwijderen",
|
"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",
|
"passkey_deleted_successfully": "Passkey succesvol verwijderd",
|
||||||
"delete_passkey_name": "Verwijder {passkeyName}",
|
"delete_passkey_name": "Verwijder {passkeyName}",
|
||||||
"passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt",
|
"passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt",
|
||||||
"name_passkey": "Naam Passkey",
|
"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_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",
|
"create_api_key": "API-sleutel aanmaken",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.",
|
"add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.",
|
||||||
"add_api_key": "API-sleutel toevoegen",
|
"add_api_key": "API-sleutel toevoegen",
|
||||||
"manage_api_keys": "API-sleutels beheren",
|
"manage_api_keys": "API-sleutels beheren",
|
||||||
"api_key_created": "API-sleutel gemaakt",
|
"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",
|
"description": "Beschrijving",
|
||||||
"api_key": "API-sleutel",
|
"api_key": "API-sleutel",
|
||||||
"close": "Dichtbij",
|
"close": "Sluiten",
|
||||||
"name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.",
|
"name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.",
|
||||||
"expires_at": "Verloopt op",
|
"expires_at": "Verloopt op",
|
||||||
"when_this_api_key_will_expire": "Wanneer deze API-sleutel verloopt.",
|
"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",
|
"expiration_date_must_be_in_the_future": "Vervaldatum moet in de toekomst liggen",
|
||||||
"revoke_api_key": "API-sleutel intrekken",
|
"revoke_api_key": "API-sleutel intrekken",
|
||||||
"never": "Nooit",
|
"never": "Nooit",
|
||||||
"revoke": "Herroepen",
|
"revoke": "Intrekken",
|
||||||
"api_key_revoked_successfully": "API-sleutel succesvol ingetrokken",
|
"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",
|
"last_used": "Laatst gebruikt",
|
||||||
"actions": "Acties",
|
"actions": "Acties",
|
||||||
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
||||||
"general": "Algemeen",
|
"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",
|
"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.",
|
"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",
|
"images": "Afbeeldingen",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt",
|
"email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt",
|
||||||
"save_changes_question": "Wijzigingen opslaan?",
|
"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",
|
"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.",
|
"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_configuration": "SMTP-configuratie",
|
||||||
"smtp_host": "SMTP-host",
|
"smtp_host": "SMTP-host",
|
||||||
"smtp_port": "SMTP-poort",
|
"smtp_port": "SMTP-poort",
|
||||||
"smtp_user": "SMTP-gebruiker",
|
"smtp_user": "SMTP-gebruiker",
|
||||||
"smtp_password": "SMTP-wachtwoord",
|
"smtp_password": "SMTP-wachtwoord",
|
||||||
"smtp_from": "SMTP van",
|
"smtp_from": "SMTP-afzender",
|
||||||
"smtp_tls_option": "SMTP TLS-optie",
|
"smtp_tls_option": "SMTP TLS-optie",
|
||||||
"email_tls_option": "E-mail TLS-optie",
|
"email_tls_option": "E-mail TLS-optie",
|
||||||
"skip_certificate_verification": "Certificaatverificatie overslaan",
|
"skip_certificate_verification": "Certificaatverificatie overslaan",
|
||||||
@@ -178,15 +178,15 @@
|
|||||||
"email_login_notification": "E-mail-inlogmelding",
|
"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.",
|
"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",
|
"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",
|
"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",
|
"send_test_email": "Test-e-mail verzenden",
|
||||||
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
||||||
"application_name": "Toepassingsnaam",
|
"application_name": "Toepassingsnaam",
|
||||||
"session_duration": "Sessieduur",
|
"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.",
|
"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.",
|
"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",
|
"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.",
|
"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_dn": "LDAP Bind-DN",
|
||||||
"ldap_bind_password": "LDAP Bind-wachtwoord",
|
"ldap_bind_password": "LDAP Bind-wachtwoord",
|
||||||
"ldap_base_dn": "LDAP-basis-DN",
|
"ldap_base_dn": "LDAP-basis-DN",
|
||||||
"user_search_filter": "Gebruikerszoekfilter",
|
"user_search_filter": "Zoekfilter gebruikers",
|
||||||
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee u gebruikers kunt zoeken/synchroniseren.",
|
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee je gebruikers kunt zoeken/synchroniseren.",
|
||||||
"groups_search_filter": "Groepen Zoekfilter",
|
"groups_search_filter": "Zoekfilter groepen",
|
||||||
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee u groepen kunt zoeken/synchroniseren.",
|
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee je groepen kunt zoeken/synchroniseren.",
|
||||||
"attribute_mapping": "Attribuuttoewijzing",
|
"attribute_mapping": "Attribuuttoewijzing",
|
||||||
"user_unique_identifier_attribute": "Gebruiker uniek identificatiekenmerk",
|
"user_unique_identifier_attribute": "Uniek gebruikersidentificatie attribuut",
|
||||||
"the_value_of_this_attribute_should_never_change": "De waarde van dit kenmerk mag nooit veranderen.",
|
"the_value_of_this_attribute_should_never_change": "De waarde van dit attribuut mag nooit veranderen.",
|
||||||
"username_attribute": "Gebruikersnaam Attribuut",
|
"username_attribute": "Gebruikersnaam attribuut",
|
||||||
"user_mail_attribute": "Gebruikersmailkenmerk",
|
"user_mail_attribute": "Gebruikers e-mail attribuut",
|
||||||
"user_first_name_attribute": "Gebruikersvoornaam Attribuut",
|
"user_first_name_attribute": "Gebruikers voornaam attribuut",
|
||||||
"user_last_name_attribute": "Gebruikersnaam Achternaam Attribuut",
|
"user_last_name_attribute": "Gebruikers achternaam attribuut",
|
||||||
"user_profile_picture_attribute": "Gebruikersprofielfoto-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 kenmerk kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.",
|
"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",
|
"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.",
|
"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": "Groeps uniek identificatiekenmerk",
|
"group_unique_identifier_attribute": "Uniek groepsidentificatie attribuut",
|
||||||
"group_name_attribute": "Groepsnaam Attribuut",
|
"group_name_attribute": "Groepsnaam attribuut",
|
||||||
"admin_group_name": "Naam van beheerdersgroep",
|
"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.",
|
"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",
|
"sync_now": "Nu synchroniseren",
|
||||||
"enable": "Inschakelen",
|
"enable": "Inschakelen",
|
||||||
"user_created_successfully": "Gebruiker succesvol aangemaakt",
|
"user_created_successfully": "Gebruiker succesvol aangemaakt",
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
"admin_privileges": "Beheerdersrechten",
|
"admin_privileges": "Beheerdersrechten",
|
||||||
"admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.",
|
"admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.",
|
||||||
"delete_firstname_lastname": "Verwijderen {firstName} {lastName}",
|
"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",
|
"user_deleted_successfully": "Gebruiker succesvol verwijderd",
|
||||||
"role": "Rol",
|
"role": "Rol",
|
||||||
"source": "Bron",
|
"source": "Bron",
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
"user": "Gebruiker",
|
"user": "Gebruiker",
|
||||||
"local": "Lokaal",
|
"local": "Lokaal",
|
||||||
"toggle_menu": "Menu wisselen",
|
"toggle_menu": "Menu wisselen",
|
||||||
"edit": "Bewerking",
|
"edit": "Bewerk",
|
||||||
"user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt",
|
"user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt",
|
||||||
"user_updated_successfully": "Gebruiker succesvol bijgewerkt",
|
"user_updated_successfully": "Gebruiker succesvol bijgewerkt",
|
||||||
"custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt",
|
"custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt",
|
||||||
@@ -252,9 +252,9 @@
|
|||||||
"manage_user_groups": "Gebruikersgroepen beheren",
|
"manage_user_groups": "Gebruikersgroepen beheren",
|
||||||
"friendly_name": "Vriendelijke naam",
|
"friendly_name": "Vriendelijke naam",
|
||||||
"name_that_will_be_displayed_in_the_ui": "Naam die in de gebruikersinterface wordt weergegeven",
|
"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}",
|
"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_group_deleted_successfully": "Gebruikersgroep succesvol verwijderd",
|
||||||
"user_count": "Gebruikersaantal",
|
"user_count": "Gebruikersaantal",
|
||||||
"user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt",
|
"user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt",
|
||||||
@@ -268,19 +268,21 @@
|
|||||||
"add_oidc_client": "OIDC-client toevoegen",
|
"add_oidc_client": "OIDC-client toevoegen",
|
||||||
"manage_oidc_clients": "OIDC-clients beheren",
|
"manage_oidc_clients": "OIDC-clients beheren",
|
||||||
"one_time_link": "Eenmalige link",
|
"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",
|
"add": "Toevoegen",
|
||||||
"callback_urls": "Callback-URL's",
|
"callback_urls": "Callback-URL's",
|
||||||
"logout_callback_urls": "Callback-URL's voor afmelden",
|
"logout_callback_urls": "Callback-URL's voor afmelden",
|
||||||
"public_client": "Publieke client",
|
"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",
|
"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.",
|
"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",
|
"name_logo": "{name} logo",
|
||||||
"change_logo": "Logo wijzigen",
|
"change_logo": "Logo wijzigen",
|
||||||
"upload_logo": "Logo uploaden",
|
"upload_logo": "Logo uploaden",
|
||||||
"remove_logo": "Logo verwijderen",
|
"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",
|
"oidc_client_deleted_successfully": "OIDC-client succesvol verwijderd",
|
||||||
"authorization_url": "Autorisatie-URL",
|
"authorization_url": "Autorisatie-URL",
|
||||||
"oidc_discovery_url": "OIDC-ontdekkings-URL",
|
"oidc_discovery_url": "OIDC-ontdekkings-URL",
|
||||||
@@ -292,12 +294,12 @@
|
|||||||
"disabled": "Uitgeschakeld",
|
"disabled": "Uitgeschakeld",
|
||||||
"oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt",
|
"oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt",
|
||||||
"create_new_client_secret": "Nieuw clientgeheim aanmaken",
|
"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",
|
"generate": "Genereren",
|
||||||
"new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt",
|
"new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt",
|
||||||
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
|
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
|
||||||
"oidc_client_name": "OIDC-client {name}",
|
"oidc_client_name": "OIDC-client {name}",
|
||||||
"client_id": "Client id",
|
"client_id": "Client ID",
|
||||||
"client_secret": "Client geheim",
|
"client_secret": "Client geheim",
|
||||||
"show_more_details": "Meer details weergeven",
|
"show_more_details": "Meer details weergeven",
|
||||||
"allowed_user_groups": "Toegestane gebruikersgroepen",
|
"allowed_user_groups": "Toegestane gebruikersgroepen",
|
||||||
@@ -308,50 +310,50 @@
|
|||||||
"background_image": "Achtergrondfoto",
|
"background_image": "Achtergrondfoto",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
"reset_profile_picture_question": "Profielfoto opnieuw instellen?",
|
"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": "Opnieuw instellen",
|
||||||
"reset_to_default": "Standaardinstellingen herstellen",
|
"reset_to_default": "Standaardinstellingen herstellen",
|
||||||
"profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
"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.",
|
"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 je een fout vindt, kun je altijd helpen met de vertaling op <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
"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",
|
"personal": "Persoonlijk",
|
||||||
"global": "Globaal",
|
"global": "Globaal",
|
||||||
"all_users": "Alle gebruikers",
|
"all_users": "Alle gebruikers",
|
||||||
"all_events": "Alle activiteiten",
|
"all_events": "Alle activiteiten",
|
||||||
"all_clients": "Alle clients",
|
"all_clients": "Alle clients",
|
||||||
"all_locations": "Alle locaties",
|
"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.",
|
"see_all_account_activities_from_the_last_3_months": "Bekijk alle gebruikersactiviteit van de afgelopen 3 maanden.",
|
||||||
"token_sign_in": "Inloggen met token",
|
"token_sign_in": "Inloggen met token",
|
||||||
"client_authorization": "Client autorisatie",
|
"client_authorization": "Client autorisatie",
|
||||||
"new_client_authorization": "Nieuwe clientautorisatie",
|
"new_client_authorization": "Nieuwe clientautorisatie",
|
||||||
"disable_animations": "Animatie uitzetten",
|
"disable_animations": "Animaties uitzetten",
|
||||||
"turn_off_ui_animations": "Zet alle animaties in de gebruikersinterface uit.",
|
"turn_off_ui_animations": "Zet alle animaties in de gebruikersinterface uit.",
|
||||||
"user_disabled": "Account uitgeschakeld",
|
"user_disabled": "Account uitgeschakeld",
|
||||||
"disabled_users_cannot_log_in_or_use_services": "Gebruikers met een handicap kunnen niet inloggen of diensten gebruiken.",
|
"disabled_users_cannot_log_in_or_use_services": "Uitgeschakelde gebruikers kunnen niet inloggen of diensten gebruiken.",
|
||||||
"user_disabled_successfully": "Je bent nu uitgelogd.",
|
"user_disabled_successfully": "Gebruiker is succesvol uitgeschakeld.",
|
||||||
"user_enabled_successfully": "Je bent nu aangemeld.",
|
"user_enabled_successfully": "Gebruiker is succesvol geactiveerd.",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"disable_firstname_lastname": "{firstName} {lastName}uitschakelen",
|
"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.",
|
"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 gebruikers met een handicap toegang krijgen tot LDAP.",
|
"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 uit het systeem verwijderd.",
|
"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 je gestuurd.",
|
"login_code_email_success": "De inlogcode is naar de gebruiker gestuurd.",
|
||||||
"send_email": "E-mail sturen",
|
"send_email": "Verstuur e-mail",
|
||||||
"show_code": "Code tonen",
|
"show_code": "Toon code",
|
||||||
"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.",
|
"callback_url_description": "URL's die de client heeft aangegeven. Als je dit leeg laat, worden ze automatisch toegevoegd. Je kunt jokertekens (*) gebruiken, maar voor de veiligheid kun je dat beter niet doen.",
|
||||||
"logout_callback_url_description": "URL's die je klant heeft gegeven om uit te loggen. Je kunt jokertekens (*) gebruiken, maar dat is niet zo'n goed idee voor de veiligheid.",
|
"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",
|
"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",
|
"authorize_device": "Apparaat autoriseren",
|
||||||
"the_device_has_been_authorized": "Het apparaat is goedgekeurd.",
|
"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",
|
"authorize": "Autoriseren",
|
||||||
"federated_client_credentials": "Federatieve clientreferenties",
|
"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_federated_client_credential": "Federatieve clientreferenties toevoegen",
|
||||||
"add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe",
|
"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",
|
"unrestricted": "Onbeperkt",
|
||||||
"show_advanced_options": "Geavanceerde opties weergeven",
|
"show_advanced_options": "Geavanceerde opties weergeven",
|
||||||
"hide_advanced_options": "Verberg geavanceerde opties",
|
"hide_advanced_options": "Verberg geavanceerde opties",
|
||||||
@@ -378,56 +380,65 @@
|
|||||||
"custom_accent_color": "Aangepaste accentkleur",
|
"custom_accent_color": "Aangepaste accentkleur",
|
||||||
"custom_accent_color_description": "Voer een eigen kleur in met een geldige CSS-kleurcode (bijvoorbeeld hex, rgb, hsl).",
|
"custom_accent_color_description": "Voer een eigen kleur in met een geldige CSS-kleurcode (bijvoorbeeld hex, rgb, hsl).",
|
||||||
"color_value": "Kleurwaarde",
|
"color_value": "Kleurwaarde",
|
||||||
"apply": "Solliciteren",
|
"apply": "Toepassen",
|
||||||
"signup_token": "Aanmeldingstoken",
|
"signup_token": "Aanmeldingstoken",
|
||||||
"create_a_signup_token_to_allow_new_user_registration": "Maak een aanmeldingstoken aan om nieuwe gebruikers te laten registreren.",
|
"create_a_signup_token_to_allow_new_user_registration": "Maak een aanmeldingstoken aan om nieuwe gebruikers te laten registreren.",
|
||||||
"usage_limit": "Gebruikslimiet",
|
"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",
|
"expires": "Verloopt",
|
||||||
"signup": "Aanmelden",
|
"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",
|
"validating_signup_token": "Inlogtoken checken",
|
||||||
"go_to_login": "Ga naar inloggen",
|
"go_to_login": "Ga naar inloggen",
|
||||||
"signup_to_appname": "Meld je aan voor {appName}",
|
"signup_to_appname": "Meld u aan voor {appName}",
|
||||||
"create_your_account_to_get_started": "Maak je account aan om te beginnen.",
|
"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. Je kunt later een wachtwoord instellen.",
|
"initial_account_creation_description": "Maak een account aan om te beginnen. U kunt later een wachtwoord instellen.",
|
||||||
"setup_your_passkey": "Stel je passkey in",
|
"setup_your_passkey": "Stel uw 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.",
|
"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",
|
"skip_for_now": "Voor nu even overslaan",
|
||||||
"account_created": "Account aangemaakt",
|
"account_created": "Account aangemaakt",
|
||||||
"enable_user_signups": "Gebruikersregistratie inschakelen",
|
"enable_user_signups": "Gebruikersregistratie inschakelen",
|
||||||
"enable_user_signups_description": "Of de functie voor gebruikersregistratie moet worden ingeschakeld.",
|
"enable_user_signups_description": "Bepaal hoe mensen zich kunnen aanmelden voor nieuwe accounts in Pocket ID.",
|
||||||
"user_signups_are_disabled": "Je kunt nu niet aanmelden.",
|
"user_signups_are_disabled": "Gebruikersregistraties zijn nu uitgeschakeld.",
|
||||||
"create_signup_token": "Aanmeldingstoken maken",
|
"create_signup_token": "Aanmeldingstoken maken",
|
||||||
"view_active_signup_tokens": "Actieve aanmeldingstokens bekijken",
|
"view_active_signup_tokens": "Actieve aanmeldingstokens bekijken",
|
||||||
"manage_signup_tokens": "Aanmeldingstokens beheren",
|
"manage_signup_tokens": "Aanmeldingstokens beheren",
|
||||||
"view_and_manage_active_signup_tokens": "Bekijk en beheer actieve aanmeldingstokens.",
|
"view_and_manage_active_signup_tokens": "Bekijk en beheer actieve aanmeldingstokens.",
|
||||||
"signup_token_deleted_successfully": "Aanmeldingstoken succesvol verwijderd.",
|
"signup_token_deleted_successfully": "Aanmeldingstoken succesvol verwijderd.",
|
||||||
"expired": "Verlopen",
|
"expired": "Verlopen",
|
||||||
"used_up": "Opgebruikt",
|
"used_up": "Verbruikt",
|
||||||
"active": "Actief",
|
"active": "Actief",
|
||||||
"usage": "Gebruik",
|
"usage": "Gebruik",
|
||||||
"created": "Gemaakt",
|
"created": "Gemaakt",
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
"loading": "Bezig met laden",
|
"loading": "Bezig met laden",
|
||||||
"delete_signup_token": "Registratietoken verwijderen",
|
"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.",
|
"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_disabled_description": "Gebruikersregistraties zijn helemaal uitgeschakeld. Alleen beheerders kunnen nieuwe gebruikersaccounts aanmaken.",
|
|
||||||
"signup_with_token": "Aanmelden met token",
|
"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": "Open inschrijving",
|
||||||
"signup_open_description": "Iedereen kan zonder beperkingen een nieuw account aanmaken.",
|
"signup_open_description": "Iedereen kan zonder beperkingen een nieuw account aanmaken.",
|
||||||
"of": "van",
|
"of": "van",
|
||||||
"skip_passkey_setup": "Pas de instellingen voor de toegangssleutel over",
|
"skip_passkey_setup": "Sla 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_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",
|
"my_apps": "Mijn apps",
|
||||||
"no_apps_available": "Geen apps beschikbaar",
|
"no_apps_available": "Geen apps beschikbaar",
|
||||||
"contact_your_administrator_for_app_access": "Neem contact op met je beheerder om toegang te krijgen tot applicaties.",
|
"contact_your_administrator_for_app_access": "Neem contact op met de beheerder om toegang te krijgen tot applicaties.",
|
||||||
"launch": "Lancering",
|
"launch": "Openen",
|
||||||
"client_launch_url": "URL voor lancering door klant",
|
"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_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 klant die je in de Pocket ID-UI ziet.",
|
"client_name_description": "De naam van de client die wordt getoond in de Pocket ID UI.",
|
||||||
"revoke_access": "Toegang intrekken",
|
"revoke_access": "Toegang intrekken",
|
||||||
"revoke_access_description": "Toegang intrekken tot <b>{clientName}</b>. <b>{clientName}</b> kan je accountgegevens niet meer bekijken.",
|
"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 echt geblokkeerd."
|
"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ć",
|
"click_to_copy": "Kliknij, aby skopiować",
|
||||||
"something_went_wrong": "Coś poszło nie tak",
|
"something_went_wrong": "Coś poszło nie tak",
|
||||||
"go_back_to_home": "Wróć do strony głównej",
|
"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",
|
"login_background": "Tło logowania",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Kod logowania",
|
"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.",
|
"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",
|
"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.",
|
"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}",
|
"name_logo": "Logo {name}",
|
||||||
"change_logo": "Zmień logo",
|
"change_logo": "Zmień logo",
|
||||||
"upload_logo": "Prześlij 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.",
|
"number_of_times_token_can_be_used": "Liczba przypadków, w których można użyć tokenu rejestracji.",
|
||||||
"expires": "Wygasają",
|
"expires": "Wygasają",
|
||||||
"signup": "Zarejestruj się",
|
"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.",
|
"signup_requires_valid_token": "Aby utworzyć konto, wymagany jest ważny token rejestracyjny.",
|
||||||
"validating_signup_token": "Weryfikacja tokenu rejestracji",
|
"validating_signup_token": "Weryfikacja tokenu rejestracji",
|
||||||
"go_to_login": "Przejdź do logowania",
|
"go_to_login": "Przejdź do logowania",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Pomiń na razie",
|
"skip_for_now": "Pomiń na razie",
|
||||||
"account_created": "Konto utworzone",
|
"account_created": "Konto utworzone",
|
||||||
"enable_user_signups": "Włącz rejestrację użytkowników",
|
"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.",
|
"user_signups_are_disabled": "Rejestracja użytkowników jest obecnie wyłączona.",
|
||||||
"create_signup_token": "Utwórz token rejestracji",
|
"create_signup_token": "Utwórz token rejestracji",
|
||||||
"view_active_signup_tokens": "Wyświetl aktywne tokeny rejestracji",
|
"view_active_signup_tokens": "Wyświetl aktywne tokeny rejestracji",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Ładowanie",
|
"loading": "Ładowanie",
|
||||||
"delete_signup_token": "Usuń token rejestracji",
|
"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ąć.",
|
"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": "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_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": "Otwórz rejestrację",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Nazwa klienta wyświetlana w interfejsie użytkownika Pocket ID.",
|
"client_name_description": "Nazwa klienta wyświetlana w interfejsie użytkownika Pocket ID.",
|
||||||
"revoke_access": "Cofnij dostęp",
|
"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_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."
|
"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",
|
"click_to_copy": "Clique para copiar",
|
||||||
"something_went_wrong": "Algo deu errado",
|
"something_went_wrong": "Algo deu errado",
|
||||||
"go_back_to_home": "Voltar para o início",
|
"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",
|
"login_background": "Histórico de login",
|
||||||
"logo": "Logotipo",
|
"logo": "Logotipo",
|
||||||
"login_code": "Código de Login:",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"name_logo": "{name} logotipo",
|
||||||
"change_logo": "Alterar logotipo",
|
"change_logo": "Alterar logotipo",
|
||||||
"upload_logo": "Carregar 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.",
|
"number_of_times_token_can_be_used": "Número de vezes que o token de inscrição pode ser usado.",
|
||||||
"expires": "Vence",
|
"expires": "Vence",
|
||||||
"signup": "Cadastre-se",
|
"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.",
|
"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",
|
"validating_signup_token": "Validando o token de inscrição",
|
||||||
"go_to_login": "Vá para o login",
|
"go_to_login": "Vá para o login",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Pular por enquanto",
|
"skip_for_now": "Pular por enquanto",
|
||||||
"account_created": "Conta criada",
|
"account_created": "Conta criada",
|
||||||
"enable_user_signups": "Ativar inscrições de usuários",
|
"enable_user_signups": "Ativar inscrições de usuários",
|
||||||
"enable_user_signups_description": "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.",
|
"user_signups_are_disabled": "As inscrições de usuários estão desativadas no momento.",
|
||||||
"create_signup_token": "Criar token de inscrição",
|
"create_signup_token": "Criar token de inscrição",
|
||||||
"view_active_signup_tokens": "Ver tokens de inscrição ativos",
|
"view_active_signup_tokens": "Ver tokens de inscrição ativos",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Carregando",
|
"loading": "Carregando",
|
||||||
"delete_signup_token": "Apagar token de inscrição",
|
"delete_signup_token": "Apagar token de inscrição",
|
||||||
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que quer apagar esse token de inscrição? Não dá pra voltar atrás.",
|
"are_you_sure_you_want_to_delete_this_signup_token": "Tem certeza que 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": "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_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": "Inscrição aberta",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "O nome do cliente que aparece na interface do Pocket ID.",
|
"client_name_description": "O nome do cliente que aparece na interface do Pocket ID.",
|
||||||
"revoke_access": "Revogar acesso",
|
"revoke_access": "Revogar acesso",
|
||||||
"revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não vai mais conseguir acessar as informações da sua conta.",
|
"revoke_access_description": "Revogar acesso a <b>{clientName}</b>. <b>{clientName}</b> não vai mais conseguir acessar as informações da sua conta.",
|
||||||
"revoke_access_successful": "O acesso a {clientName} foi revogado com sucesso."
|
"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,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": "Нет доступа к вашему пасскею?",
|
"alternative_sign_in_methods": "Альтернативные способы входа",
|
||||||
"login_background": "Фон страницы входа",
|
"login_background": "Фон страницы входа",
|
||||||
"logo": "Логотип",
|
"logo": "Логотип",
|
||||||
"login_code": "Код входа",
|
"login_code": "Код входа",
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
"passkeys": "Пасскеи",
|
"passkeys": "Пасскеи",
|
||||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте пасскеями, которые вы можете использовать для аутентификации себя.",
|
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте пасскеями, которые вы можете использовать для аутентификации себя.",
|
||||||
"add_passkey": "Добавить пасскей",
|
"add_passkey": "Добавить пасскей",
|
||||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без passkey.",
|
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без пасскея.",
|
||||||
"create": "Создать",
|
"create": "Создать",
|
||||||
"first_name": "Имя",
|
"first_name": "Имя",
|
||||||
"last_name": "Фамилия",
|
"last_name": "Фамилия",
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
"public_clients_description": "Публичные клиенты не имеют клиентского секрета. Они предназначены для мобильных, SPA и нативных приложений, где секретные данные нельзя надежно хранить.",
|
"public_clients_description": "Публичные клиенты не имеют клиентского секрета. Они предназначены для мобильных, SPA и нативных приложений, где секретные данные нельзя надежно хранить.",
|
||||||
"pkce": "PKCE",
|
"pkce": "PKCE",
|
||||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
|
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
|
||||||
|
"requires_reauthentication": "Требуется повторная аутентификация",
|
||||||
|
"requires_users_to_authenticate_again_on_each_authorization": "Требует от пользователей повторной аутентификации при каждой авторизации, даже если они уже вошли в систему",
|
||||||
"name_logo": "Логотип {name}",
|
"name_logo": "Логотип {name}",
|
||||||
"change_logo": "Изменить логотип",
|
"change_logo": "Изменить логотип",
|
||||||
"upload_logo": "Загрузить логотип",
|
"upload_logo": "Загрузить логотип",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "Количество раз, которое может быть использован токен регистрации.",
|
"number_of_times_token_can_be_used": "Количество раз, которое может быть использован токен регистрации.",
|
||||||
"expires": "Истекает",
|
"expires": "Истекает",
|
||||||
"signup": "Зарегистрироваться",
|
"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": "Для создания учетной записи необходим действительный токен регистрации",
|
"signup_requires_valid_token": "Для создания учетной записи необходим действительный токен регистрации",
|
||||||
"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": "Должна ли быть включена функция регистрации пользователя.",
|
"enable_user_signups_description": "Решите, как пользователи могут регистрировать новые учетные записи в Pocket ID.",
|
||||||
"user_signups_are_disabled": "Регистрация пользователей в настоящее время отключена",
|
"user_signups_are_disabled": "Регистрация пользователей в настоящее время отключена",
|
||||||
"create_signup_token": "Создать токен регистрации",
|
"create_signup_token": "Создать токен регистрации",
|
||||||
"view_active_signup_tokens": "Показать активные токены регистрации",
|
"view_active_signup_tokens": "Показать активные токены регистрации",
|
||||||
@@ -412,22 +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": "Вы уверены, что хотите удалить этот токен регистрации? Это действие нельзя отменить.",
|
||||||
"signup_disabled_description": "Регистрация пользователей полностью отключена. Только администраторы могут создавать новые учетные записи пользователей.",
|
|
||||||
"signup_with_token": "Регистрация с токеном",
|
"signup_with_token": "Регистрация с токеном",
|
||||||
"signup_with_token_description": "Пользователи могут зарегистрироваться только с помощью действительного токена регистрации, созданного администратором.",
|
"signup_with_token_description": "Пользователи могут зарегистрироваться только с помощью действительного токена регистрации, созданного администратором.",
|
||||||
"signup_open": "Открытая регистрация",
|
"signup_open": "Открытая регистрация",
|
||||||
"signup_open_description": "Любой может создать новую учетную запись без ограничений.",
|
"signup_open_description": "Любой может создать новую учетную запись без ограничений.",
|
||||||
"of": "из",
|
"of": "из",
|
||||||
"skip_passkey_setup": "Пропустить настройку пасскея",
|
"skip_passkey_setup": "Пропустить настройку пасскея",
|
||||||
"skip_passkey_setup_description": "Настоятельно рекомендуется настроить passkey, так как без него вы более не сможете войти в учетную запись после истечения сессии.",
|
"skip_passkey_setup_description": "Настоятельно рекомендуется настроить пасскей, так как без него вы более не сможете войти в учетную запись после истечения сессии.",
|
||||||
"my_apps": "Мои приложения",
|
"my_apps": "Мои приложения",
|
||||||
"no_apps_available": "Нет доступных приложений",
|
"no_apps_available": "Нет доступных приложений",
|
||||||
"contact_your_administrator_for_app_access": "Свяжись с администратором, чтобы получить доступ к приложениям.",
|
"contact_your_administrator_for_app_access": "Свяжись с администратором, чтобы получить доступ к приложениям.",
|
||||||
"launch": "Запуск",
|
"launch": "Запустить",
|
||||||
"client_launch_url": "URL запуска клиента",
|
"client_launch_url": "Клиентский URL для запуска",
|
||||||
"client_launch_url_description": "URL-адрес, который откроется, когда кто-то запустит приложение со страницы «Мои приложения».",
|
"client_launch_url_description": "URL-адрес, который откроется, когда кто-то запустит приложение со страницы «Мои приложения».",
|
||||||
"client_name_description": "Имя клиента, которое показывается в интерфейсе Pocket ID.",
|
"client_name_description": "Имя клиента, которое отображается в интерфейсе Pocket ID.",
|
||||||
"revoke_access": "Отменить доступ",
|
"revoke_access": "Отозвать доступ",
|
||||||
"revoke_access_description": "Отменить доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет заходить в твою учетную запись.",
|
"revoke_access_description": "Отозвать доступ к <b>{clientName}</b>. <b>{clientName}</b> больше не сможет получить доступ к информации вашей учетной записи.",
|
||||||
"revoke_access_successful": "Доступ к {clientName} был успешно заблокирован."
|
"revoke_access_successful": "Доступ к {clientName} успешно отозван.",
|
||||||
|
"last_signed_in_ago": "Последний вход {time} назад",
|
||||||
|
"invalid_client_id": "ID клиента может содержать только буквы, цифры, подчеркивания и дефисы",
|
||||||
|
"custom_client_id_description": "Установите пользовательский ID клиента, если это нужно для вашего приложения. Если нет, оставьте поле пустым, чтобы он был сгенерирован случайным образом.",
|
||||||
|
"generated": "Сгенерированный"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Не маєте доступу до свого ключа доступу?",
|
"alternative_sign_in_methods": "Альтернативні способи входу",
|
||||||
"login_background": "Фон сторінки входу",
|
"login_background": "Фон сторінки входу",
|
||||||
"logo": "Логотип",
|
"logo": "Логотип",
|
||||||
"login_code": "Код входу",
|
"login_code": "Код входу",
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
"smtp_tls_option": "Тип SMTP TLS",
|
"smtp_tls_option": "Тип SMTP TLS",
|
||||||
"email_tls_option": "TLS налаштування електронної пошти",
|
"email_tls_option": "TLS налаштування електронної пошти",
|
||||||
"skip_certificate_verification": "Пропустити перевірку сертифіката",
|
"skip_certificate_verification": "Пропустити перевірку сертифіката",
|
||||||
"this_can_be_useful_for_selfsigned_certificates": "Ця опція може бути корисною для спопідписних сертифікатів.",
|
"this_can_be_useful_for_selfsigned_certificates": "Ця опція може бути корисною для самопідписаних сертифікатів.",
|
||||||
"enabled_emails": "Увімкнені електронні листи",
|
"enabled_emails": "Увімкнені електронні листи",
|
||||||
"email_login_notification": "Сповіщення електронною поштою про вхід",
|
"email_login_notification": "Сповіщення електронною поштою про вхід",
|
||||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Надіслати електронний лист користувачеві після входу з нового пристрою.",
|
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Надіслати електронний лист користувачеві після входу з нового пристрою.",
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
"client_configuration": "Налаштування клієнтів",
|
"client_configuration": "Налаштування клієнтів",
|
||||||
"ldap_url": "URL-адреса LDAP",
|
"ldap_url": "URL-адреса LDAP",
|
||||||
"ldap_bind_dn": "LDAP Bind DN",
|
"ldap_bind_dn": "LDAP Bind DN",
|
||||||
"ldap_bind_password": "Пароль прив’язки LDAP",
|
"ldap_bind_password": "Пароль LDAP Bind",
|
||||||
"ldap_base_dn": "LDAP Base DN",
|
"ldap_base_dn": "LDAP Base DN",
|
||||||
"user_search_filter": "Фільтр пошуку користувачів",
|
"user_search_filter": "Фільтр пошуку користувачів",
|
||||||
"the_search_filter_to_use_to_search_or_sync_users": "Фільтр пошуку для пошуку/синхронізації користувачів.",
|
"the_search_filter_to_use_to_search_or_sync_users": "Фільтр пошуку для пошуку/синхронізації користувачів.",
|
||||||
@@ -251,8 +251,8 @@
|
|||||||
"add_group": "Створити групу",
|
"add_group": "Створити групу",
|
||||||
"manage_user_groups": "Керування групами користувачів",
|
"manage_user_groups": "Керування групами користувачів",
|
||||||
"friendly_name": "Зручна назва",
|
"friendly_name": "Зручна назва",
|
||||||
"name_that_will_be_displayed_in_the_ui": "Ім'я, яке буде показуватися в інтерфейсі користувача",
|
"name_that_will_be_displayed_in_the_ui": "Назва, яка буде показуватися в інтерфейсі користувача",
|
||||||
"name_that_will_be_in_the_groups_claim": "Ім'я, яке буде в атрибуті \"groups\"",
|
"name_that_will_be_in_the_groups_claim": "Назва, яка буде в атрибуті \"groups\"",
|
||||||
"delete_name": "Видалити {name}",
|
"delete_name": "Видалити {name}",
|
||||||
"are_you_sure_you_want_to_delete_this_user_group": "Ви впевнені, що хочете видалити цю групу користувачів?",
|
"are_you_sure_you_want_to_delete_this_user_group": "Ви впевнені, що хочете видалити цю групу користувачів?",
|
||||||
"user_group_deleted_successfully": "Групу користувачів успішно видалено",
|
"user_group_deleted_successfully": "Групу користувачів успішно видалено",
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
"public_clients_description": "Публічні клієнти не мають секретного ключа. Вони призначені для мобільних, веб та нативних додатків, де секретний ключ не може надійно зберігатись.",
|
"public_clients_description": "Публічні клієнти не мають секретного ключа. Вони призначені для мобільних, веб та нативних додатків, де секретний ключ не може надійно зберігатись.",
|
||||||
"pkce": "PKCE",
|
"pkce": "PKCE",
|
||||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — це функція безпеки, що запобігає атакам типу CSRF та перехопленню коду авторизації.",
|
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — це функція безпеки, що запобігає атакам типу CSRF та перехопленню коду авторизації.",
|
||||||
|
"requires_reauthentication": "Потрібна повторна автентифікація",
|
||||||
|
"requires_users_to_authenticate_again_on_each_authorization": "Вимагає від користувачів повторної автентифікації при кожній авторизації, навіть якщо вони вже ввійшли в систему.",
|
||||||
"name_logo": "Логотип {name}",
|
"name_logo": "Логотип {name}",
|
||||||
"change_logo": "Змінити логотип",
|
"change_logo": "Змінити логотип",
|
||||||
"upload_logo": "Вивантажити логотип",
|
"upload_logo": "Вивантажити логотип",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "Скільки разів можна використовувати реєстраційний токен.",
|
"number_of_times_token_can_be_used": "Скільки разів можна використовувати реєстраційний токен.",
|
||||||
"expires": "Термін дії",
|
"expires": "Термін дії",
|
||||||
"signup": "Зареєструватися",
|
"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": "Для створення облікового запису потрібен дійсний токен реєстрації",
|
"signup_requires_valid_token": "Для створення облікового запису потрібен дійсний токен реєстрації",
|
||||||
"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": "Чи слід увімкнути можливість реєстрації користувачів.",
|
"enable_user_signups_description": "Визначте, як користувачі можуть реєструвати нові облікові записи в Pocket ID.",
|
||||||
"user_signups_are_disabled": "Реєстрація користувачів наразі вимкнена",
|
"user_signups_are_disabled": "Реєстрація користувачів наразі вимкнена",
|
||||||
"create_signup_token": "Створити токен реєстрації",
|
"create_signup_token": "Створити токен реєстрації",
|
||||||
"view_active_signup_tokens": "Переглянути активні токени реєстрації",
|
"view_active_signup_tokens": "Переглянути активні токени реєстрації",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"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": "Ви впевнені, що хочете видалити цей токен реєстрації? Цю дію неможливо скасувати.",
|
||||||
"signup_disabled_description": "Реєстрація користувачів повністю вимкнена. Нові облікові записи можуть створювати лише адміністратори.",
|
|
||||||
"signup_with_token": "Зареєструватися за допомогою токену",
|
"signup_with_token": "Зареєструватися за допомогою токену",
|
||||||
"signup_with_token_description": "Користувачі можуть зареєструватися лише за допомогою дійсного токена реєстрації, створеного адміністратором.",
|
"signup_with_token_description": "Користувачі можуть зареєструватися лише за допомогою дійсного токена реєстрації, створеного адміністратором.",
|
||||||
"signup_open": "Відкрита реєстрація",
|
"signup_open": "Відкрита реєстрація",
|
||||||
@@ -420,14 +427,18 @@
|
|||||||
"of": "з",
|
"of": "з",
|
||||||
"skip_passkey_setup": "Пропустити налаштування ключа доступу",
|
"skip_passkey_setup": "Пропустити налаштування ключа доступу",
|
||||||
"skip_passkey_setup_description": "Рекомендується налаштувати ключ доступу, оскільки без нього ви не зможете увійти у свій обліковий запис після закінчення сеансу.",
|
"skip_passkey_setup_description": "Рекомендується налаштувати ключ доступу, оскільки без нього ви не зможете увійти у свій обліковий запис після закінчення сеансу.",
|
||||||
"my_apps": "Мої програми",
|
"my_apps": "Мої додатки",
|
||||||
"no_apps_available": "Немає доступних додатків",
|
"no_apps_available": "Немає доступних додатків",
|
||||||
"contact_your_administrator_for_app_access": "Зверніться до адміністратора, щоб отримати доступ до додатків.",
|
"contact_your_administrator_for_app_access": "Зверніться до адміністратора, щоб отримати доступ до додатків.",
|
||||||
"launch": "Запуск",
|
"launch": "Запуск",
|
||||||
"client_launch_url": "URL-адреса запуску клієнта",
|
"client_launch_url": "URL-адреса для запуску клієнта",
|
||||||
"client_launch_url_description": "URL-адреса, яка відкриється, коли користувач запустить програму зі сторінки «Мої програми».",
|
"client_launch_url_description": "URL-адреса, яка відкриється, коли користувач запустить програму зі сторінки «Мої програми».",
|
||||||
"client_name_description": "Ім'я клієнта, яке відображається в інтерфейсі Pocket ID.",
|
"client_name_description": "Назва клієнта, яке відображається в інтерфейсі Pocket ID.",
|
||||||
"revoke_access": "Скасувати доступ",
|
"revoke_access": "Скасувати доступ",
|
||||||
"revoke_access_description": "Скасувати доступ до <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.",
|
"revoke_access_description": "Скасувати доступ для <b>{clientName}</b>. <b>{clientName}</b> більше не зможе отримати доступ до інформації вашого облікового запису.",
|
||||||
"revoke_access_successful": "Доступ до {clientName} було успішно скасовано."
|
"revoke_access_successful": "Доступ для {clientName} було успішно скасовано.",
|
||||||
|
"last_signed_in_ago": "Останній вхід {time} тому",
|
||||||
|
"invalid_client_id": "Ідентифікатор клієнта може містити тільки літери, цифри, підкреслення та дефіси.",
|
||||||
|
"custom_client_id_description": "Встановіть власний ідентифікатор клієнта, якщо це потрібно для вашої програми. В іншому випадку залиште поле порожнім, щоб створити випадковий ідентифікатор.",
|
||||||
|
"generated": "Створено"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"click_to_copy": "Nhấn để sao chép",
|
"click_to_copy": "Nhấn để sao chép",
|
||||||
"something_went_wrong": "Đã xảy ra lỗi",
|
"something_went_wrong": "Đã xảy ra lỗi",
|
||||||
"go_back_to_home": "Quay lại trang chủ",
|
"go_back_to_home": "Quay lại trang chủ",
|
||||||
"dont_have_access_to_your_passkey": "Không có passkey?",
|
"alternative_sign_in_methods": "Các phương thức đăng nhập thay thế",
|
||||||
"login_background": "Hình nền đăng nhập",
|
"login_background": "Hình nền đăng nhập",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Mã đăng nhập",
|
"login_code": "Mã đăng nhập",
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
"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.",
|
"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",
|
"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.",
|
"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",
|
"name_logo": "{name} logo",
|
||||||
"change_logo": "Đổi Logo",
|
"change_logo": "Đổi Logo",
|
||||||
"upload_logo": "Tải logo",
|
"upload_logo": "Tải logo",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "Số lần token đăng ký có thể được 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",
|
"expires": "Hết hạn",
|
||||||
"signup": "Đăng ký",
|
"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",
|
"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ý",
|
"validating_signup_token": "Xác thực token đăng ký",
|
||||||
"go_to_login": "Đi tới đăng nhập",
|
"go_to_login": "Đi tới đăng nhập",
|
||||||
@@ -396,7 +404,7 @@
|
|||||||
"skip_for_now": "Tạm thời bỏ qua",
|
"skip_for_now": "Tạm thời bỏ qua",
|
||||||
"account_created": "Tài khoản đã được tạo",
|
"account_created": "Tài khoản đã được tạo",
|
||||||
"enable_user_signups": "Bật đăng ký người dùng",
|
"enable_user_signups": "Bật đăng ký người dùng",
|
||||||
"enable_user_signups_description": "Có nên kích hoạt tính năng Đăng ký người dùng hay khô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",
|
"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",
|
"create_signup_token": "Tạo Signup Token",
|
||||||
"view_active_signup_tokens": "Xem các Signup Tokens đang hoạt động",
|
"view_active_signup_tokens": "Xem các Signup Tokens đang hoạt động",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"loading": "Đang tải",
|
"loading": "Đang tải",
|
||||||
"delete_signup_token": "Xóa mã Signup Token",
|
"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.",
|
"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_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_with_token": "Đăng ký bằng token",
|
"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_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": "Mở Đăng Ký",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "Tên của khách hàng hiển thị trong giao diện Pocket ID.",
|
"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": "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_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."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "无法使用您的通行密钥?试试其他登录方式。",
|
"alternative_sign_in_methods": "替代登录方式",
|
||||||
"login_background": "登录页背景图",
|
"login_background": "登录页背景图",
|
||||||
"logo": "图标",
|
"logo": "图标",
|
||||||
"login_code": "临时登录码",
|
"login_code": "临时登录码",
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
"public_clients_description": "公共客户端没有客户端密钥。它们用于无法安全存储密钥的移动端、Web端和原生应用程序。",
|
"public_clients_description": "公共客户端没有客户端密钥。它们用于无法安全存储密钥的移动端、Web端和原生应用程序。",
|
||||||
"pkce": "公钥代码交换",
|
"pkce": "公钥代码交换",
|
||||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
|
"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",
|
"name_logo": "{name} Logo",
|
||||||
"change_logo": "更改 Logo",
|
"change_logo": "更改 Logo",
|
||||||
"upload_logo": "上传 Logo",
|
"upload_logo": "上传 Logo",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "注册令牌最多使用次数。",
|
"number_of_times_token_can_be_used": "注册令牌最多使用次数。",
|
||||||
"expires": "过期时间",
|
"expires": "过期时间",
|
||||||
"signup": "注册",
|
"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": "必须输入有效注册令牌才能注册新账户",
|
"signup_requires_valid_token": "必须输入有效注册令牌才能注册新账户",
|
||||||
"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": "是否启用新用户注册功能。",
|
"enable_user_signups_description": "决定用户如何在Pocket ID中注册新账户。",
|
||||||
"user_signups_are_disabled": "目前禁止新用户注册",
|
"user_signups_are_disabled": "目前禁止新用户注册",
|
||||||
"create_signup_token": "创建一个注册令牌",
|
"create_signup_token": "创建一个注册令牌",
|
||||||
"view_active_signup_tokens": "查看有效注册令牌",
|
"view_active_signup_tokens": "查看有效注册令牌",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"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": "确定要删除这个注册令牌吗?此操作不可撤销。",
|
||||||
"signup_disabled_description": "已完全禁止新用户注册。只有管理员可以创建新账户。",
|
|
||||||
"signup_with_token": "使用令牌注册",
|
"signup_with_token": "使用令牌注册",
|
||||||
"signup_with_token_description": "用户必须持有管理员创建的有效注册令牌才能注册新账户。",
|
"signup_with_token_description": "用户必须持有管理员创建的有效注册令牌才能注册新账户。",
|
||||||
"signup_open": "开放注册",
|
"signup_open": "开放注册",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "在Pocket ID用户界面中显示的客户端名称。",
|
"client_name_description": "在Pocket ID用户界面中显示的客户端名称。",
|
||||||
"revoke_access": "撤销访问权限",
|
"revoke_access": "撤销访问权限",
|
||||||
"revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。",
|
"revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。",
|
||||||
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。"
|
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。",
|
||||||
|
"last_signed_in_ago": "最后一次登录 {time} 前",
|
||||||
|
"invalid_client_id": "客户 ID 只能包含字母、数字、下划线和连字符。",
|
||||||
|
"custom_client_id_description": "如果您的应用程序需要自定义客户端 ID,请在此处设置。否则,请留空以生成一个随机生成的客户端 ID。",
|
||||||
|
"generated": "生成"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "無法存取您的密碼金鑰嗎?",
|
"alternative_sign_in_methods": "其他登入方法",
|
||||||
"login_background": "登入背景",
|
"login_background": "登入背景",
|
||||||
"logo": "標誌",
|
"logo": "標誌",
|
||||||
"login_code": "登入代碼",
|
"login_code": "登入代碼",
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
"public_clients_description": "公開客戶端 (Public Client) 不包含 client secret。這類客戶端是為了行動裝置、網頁以及無法安全儲存 secret 的原生應用程式所設計。",
|
"public_clients_description": "公開客戶端 (Public Client) 不包含 client secret。這類客戶端是為了行動裝置、網頁以及無法安全儲存 secret 的原生應用程式所設計。",
|
||||||
"pkce": "PKCE",
|
"pkce": "PKCE",
|
||||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "PKCE(公開金鑰碼交換)是一項安全機制,用於防止 CSRF 與授權碼攔截攻擊。",
|
"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} 標誌",
|
"name_logo": "{name} 標誌",
|
||||||
"change_logo": "更改標誌",
|
"change_logo": "更改標誌",
|
||||||
"upload_logo": "上傳標誌",
|
"upload_logo": "上傳標誌",
|
||||||
@@ -385,6 +387,12 @@
|
|||||||
"number_of_times_token_can_be_used": "註冊令牌可使用的次數。",
|
"number_of_times_token_can_be_used": "註冊令牌可使用的次數。",
|
||||||
"expires": "到期",
|
"expires": "到期",
|
||||||
"signup": "註冊",
|
"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": "建立帳號需要有效的註冊令牌",
|
"signup_requires_valid_token": "建立帳號需要有效的註冊令牌",
|
||||||
"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": "是否要啟用使用者註冊功能。",
|
"enable_user_signups_description": "決定使用者如何在 Pocket ID 中註冊新帳戶。",
|
||||||
"user_signups_are_disabled": "使用者註冊目前已停用",
|
"user_signups_are_disabled": "使用者註冊目前已停用",
|
||||||
"create_signup_token": "建立註冊令牌",
|
"create_signup_token": "建立註冊令牌",
|
||||||
"view_active_signup_tokens": "檢視有效的註冊令牌",
|
"view_active_signup_tokens": "檢視有效的註冊令牌",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"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": "您確定要刪除這個註冊令牌嗎?此動作無法撤銷。",
|
||||||
"signup_disabled_description": "使用者註冊已完全停用。只有管理員可以建立新的使用者帳號。",
|
|
||||||
"signup_with_token": "使用註冊令牌註冊",
|
"signup_with_token": "使用註冊令牌註冊",
|
||||||
"signup_with_token_description": "使用者只能使用管理員建立的有效登入令牌註冊。",
|
"signup_with_token_description": "使用者只能使用管理員建立的有效登入令牌註冊。",
|
||||||
"signup_open": "開放報名",
|
"signup_open": "開放報名",
|
||||||
@@ -429,5 +436,9 @@
|
|||||||
"client_name_description": "顯示在 Pocket ID UI 中的用戶端名稱。",
|
"client_name_description": "顯示在 Pocket ID UI 中的用戶端名稱。",
|
||||||
"revoke_access": "撤銷存取權",
|
"revoke_access": "撤銷存取權",
|
||||||
"revoke_access_description": "撤銷存取 <b>{clientName}</b>. <b>{clientName}</b>將無法再存取您的帳戶資訊。",
|
"revoke_access_description": "撤銷存取 <b>{clientName}</b>. <b>{clientName}</b>將無法再存取您的帳戶資訊。",
|
||||||
"revoke_access_successful": "{clientName} 的存取權已成功取消。"
|
"revoke_access_successful": "{clientName} 的存取權已成功取消。",
|
||||||
|
"last_signed_in_ago": "上次登入 {time} 前",
|
||||||
|
"invalid_client_id": "用戶端 ID 只能包含字母、數字、底線和連字符",
|
||||||
|
"custom_client_id_description": "如果您的應用程式需要,請設定自訂用戶端 ID。否則,請留空以產生隨機 ID。",
|
||||||
|
"generated": "產生"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "1.7.0",
|
"version": "1.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"jose": "^5.10.0",
|
"jose": "^5.10.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sveltekit-superforms": "^2.27.1",
|
"sveltekit-superforms": "^2.27.1",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"rollup": "^4.46.3",
|
||||||
"svelte": "^5.36.16",
|
"svelte": "^5.36.16",
|
||||||
"svelte-check": "^4.3.0",
|
"svelte-check": "^4.3.0",
|
||||||
"svelte-sonner": "^1.0.5",
|
"svelte-sonner": "^1.0.5",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"es",
|
"es",
|
||||||
"fr",
|
"fr",
|
||||||
"it",
|
"it",
|
||||||
|
"ko",
|
||||||
"nl",
|
"nl",
|
||||||
"pl",
|
"pl",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
|
|
||||||
/* Font */
|
/* Font */
|
||||||
--font-playfair: 'Playfair Display', serif;
|
--font-playfair: 'Playfair Display', serif;
|
||||||
|
--font-code: 'Google Sans', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -167,6 +168,11 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
|
src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Google Sans';
|
||||||
|
font-weight: 600;
|
||||||
|
src: url('/fonts/GoogleSansCode-SemiBold.ttf') format('truetype');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes accordion-down {
|
@keyframes accordion-down {
|
||||||
|
|||||||
140
frontend/src/lib/components/form/searchable-multi-select.svelte
Normal file
140
frontend/src/lib/components/form/searchable-multi-select.svelte
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as Command from '$lib/components/ui/command';
|
||||||
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
|
import { cn } from '$lib/utils/style';
|
||||||
|
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||||
|
import type { FormEventHandler } from 'svelte/elements';
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
items,
|
||||||
|
selectedItems = $bindable(),
|
||||||
|
onSelect,
|
||||||
|
oninput,
|
||||||
|
isLoading = false,
|
||||||
|
placeholder = 'Select items...',
|
||||||
|
searchText = 'Search...',
|
||||||
|
noItemsText = 'No items found.',
|
||||||
|
disableInternalSearch = false,
|
||||||
|
id
|
||||||
|
}: {
|
||||||
|
items: Item[];
|
||||||
|
selectedItems: string[];
|
||||||
|
onSelect?: (value: string[]) => void;
|
||||||
|
oninput?: FormEventHandler<HTMLInputElement>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
searchText?: string;
|
||||||
|
noItemsText?: string;
|
||||||
|
disableInternalSearch?: boolean;
|
||||||
|
id?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let searchValue = $state('');
|
||||||
|
let filteredItems = $state(items);
|
||||||
|
|
||||||
|
const selectedLabels = $derived(
|
||||||
|
items.filter((item) => selectedItems.includes(item.value)).map((item) => item.label)
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleItemSelect(value: string) {
|
||||||
|
let newSelectedItems: string[];
|
||||||
|
if (selectedItems.includes(value)) {
|
||||||
|
newSelectedItems = selectedItems.filter((item) => item !== value);
|
||||||
|
} else {
|
||||||
|
newSelectedItems = [...selectedItems, value];
|
||||||
|
}
|
||||||
|
selectedItems = newSelectedItems;
|
||||||
|
onSelect?.(newSelectedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterItems(search: string) {
|
||||||
|
if (disableInternalSearch) return;
|
||||||
|
searchValue = search;
|
||||||
|
if (!search) {
|
||||||
|
filteredItems = items;
|
||||||
|
} else {
|
||||||
|
filteredItems = items.filter((item) =>
|
||||||
|
item.label.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset search value when the popover is closed
|
||||||
|
$effect(() => {
|
||||||
|
if (!open) {
|
||||||
|
filterItems('');
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredItems = items;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover.Root bind:open>
|
||||||
|
<Popover.Trigger {id}>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
class="h-auto min-h-10 w-full justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
{#if selectedItems.length > 0}
|
||||||
|
{#each selectedLabels as label}
|
||||||
|
<Badge variant="secondary">{label}</Badge>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<span class="text-muted-foreground font-normal">{placeholder}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="p-0" sameWidth>
|
||||||
|
<Command.Root shouldFilter={false}>
|
||||||
|
<Command.Input
|
||||||
|
placeholder={searchText}
|
||||||
|
value={searchValue}
|
||||||
|
oninput={(e) => {
|
||||||
|
filterItems(e.currentTarget.value);
|
||||||
|
oninput?.(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Command.Empty>
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="flex w-full items-center justify-center py-2">
|
||||||
|
<LoaderCircle class="size-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{noItemsText}
|
||||||
|
{/if}
|
||||||
|
</Command.Empty>
|
||||||
|
<Command.Group class="max-h-60 overflow-y-auto">
|
||||||
|
{#each filteredItems as item}
|
||||||
|
<Command.Item
|
||||||
|
aria-checked={selectedItems.includes(item.value)}
|
||||||
|
value={item.value}
|
||||||
|
onSelect={() => {
|
||||||
|
handleItemSelect(item.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LucideCheck
|
||||||
|
class={cn('mr-2 size-4', !selectedItems.includes(item.value) && 'text-transparent')}
|
||||||
|
/>
|
||||||
|
{item.label}
|
||||||
|
</Command.Item>
|
||||||
|
{/each}
|
||||||
|
</Command.Group>
|
||||||
|
</Command.Root>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
)}`}
|
)}`}
|
||||||
class="text-muted-foreground text-xs transition-colors hover:underline"
|
class="text-muted-foreground text-xs transition-colors hover:underline"
|
||||||
>
|
>
|
||||||
{m.dont_have_access_to_your_passkey()}
|
{m.alternative_sign_in_methods()}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
)}`}
|
)}`}
|
||||||
class="text-muted-foreground mt-7 flex justify-center text-xs transition-colors hover:underline"
|
class="text-muted-foreground mt-7 flex justify-center text-xs transition-colors hover:underline"
|
||||||
>
|
>
|
||||||
{m.dont_have_access_to_your_passkey()}
|
{m.alternative_sign_in_methods()}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
|
|||||||
@@ -36,8 +36,7 @@
|
|||||||
|
|
||||||
async function createLoginCode() {
|
async function createLoginCode() {
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
code = await userService.createOneTimeAccessToken(userId!, availableExpirations[selectedExpiration]);
|
||||||
code = await userService.createOneTimeAccessToken(expiration, userId!);
|
|
||||||
oneTimeLink = `${page.url.origin}/lc/${code}`;
|
oneTimeLink = `${page.url.origin}/lc/${code}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
axiosErrorToast(e);
|
axiosErrorToast(e);
|
||||||
@@ -46,8 +45,7 @@
|
|||||||
|
|
||||||
async function sendLoginCodeEmail() {
|
async function sendLoginCodeEmail() {
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
await userService.requestOneTimeAccessEmailAsAdmin(userId!, availableExpirations[selectedExpiration]);
|
||||||
await userService.requestOneTimeAccessEmailAsAdmin(userId!, expiration);
|
|
||||||
toast.success(m.login_code_email_success());
|
toast.success(m.login_code_email_success());
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -81,7 +79,7 @@
|
|||||||
value={Object.keys(availableExpirations)[0]}
|
value={Object.keys(availableExpirations)[0]}
|
||||||
onValueChange={(v) => (selectedExpiration = v! as keyof typeof availableExpirations)}
|
onValueChange={(v) => (selectedExpiration = v! as keyof typeof availableExpirations)}
|
||||||
>
|
>
|
||||||
<Select.Trigger id="expiration" class="h-9 w-full">
|
<Select.Trigger id="expiration" class="w-full h-9">
|
||||||
{selectedExpiration}
|
{selectedExpiration}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
@@ -108,10 +106,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<CopyToClipboard value={code!}>
|
<CopyToClipboard value={code!}>
|
||||||
<p class="text-3xl font-semibold">{code}</p>
|
<p class="text-3xl font-code">{code}</p>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
|
|
||||||
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
|
<div class="flex items-center justify-center gap-3 my-2 text-muted-foreground">
|
||||||
<Separator />
|
<Separator />
|
||||||
<p class="text-xs text-nowrap">{m.or_visit()}</p>
|
<p class="text-xs text-nowrap">{m.or_visit()}</p>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@@ -37,8 +37,7 @@
|
|||||||
|
|
||||||
async function createSignupToken() {
|
async function createSignupToken() {
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
signupToken = await userService.createSignupToken(availableExpirations[selectedExpiration], usageLimit);
|
||||||
signupToken = await userService.createSignupToken(expiration, usageLimit);
|
|
||||||
signupLink = `${page.url.origin}/st/${signupToken}`;
|
signupLink = `${page.url.origin}/st/${signupToken}`;
|
||||||
|
|
||||||
if (onTokenCreated) {
|
if (onTokenCreated) {
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ export default class AppConfigService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(appConfig: AllAppConfig) {
|
async update(appConfig: AllAppConfig) {
|
||||||
// Convert all values to string
|
// Convert all values to string, stringifying JSON where needed
|
||||||
const appConfigConvertedToString = {};
|
const appConfigConvertedToString: Record<string, string> = {};
|
||||||
for (const key in appConfig) {
|
for (const key in appConfig) {
|
||||||
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString();
|
const value = (appConfig as any)[key];
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
appConfigConvertedToString[key] = JSON.stringify(value);
|
||||||
|
} else {
|
||||||
|
appConfigConvertedToString[key] = String(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
|
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
|
||||||
return this.parseConfigList(res.data);
|
return this.parseConfigList(res.data);
|
||||||
@@ -66,6 +71,16 @@ export default class AppConfigService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseValue(value: string) {
|
private parseValue(value: string) {
|
||||||
|
// Try to parse JSON first
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
value = String(parsed);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Handle rest of the types
|
||||||
if (value === 'true') {
|
if (value === 'true') {
|
||||||
return true;
|
return true;
|
||||||
} else if (value === 'false') {
|
} else if (value === 'false') {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
AuthorizedOidcClient,
|
AccessibleOidcClient,
|
||||||
AuthorizeResponse,
|
AuthorizeResponse,
|
||||||
OidcClient,
|
OidcClient,
|
||||||
OidcClientCreate,
|
OidcClientCreate,
|
||||||
OidcClientMetaData,
|
OidcClientMetaData,
|
||||||
|
OidcClientUpdate,
|
||||||
OidcClientWithAllowedUserGroups,
|
OidcClientWithAllowedUserGroups,
|
||||||
OidcClientWithAllowedUserGroupsCount,
|
OidcClientWithAllowedUserGroupsCount,
|
||||||
OidcDeviceCodeInfo
|
OidcDeviceCodeInfo
|
||||||
@@ -19,7 +20,8 @@ class OidcService extends APIService {
|
|||||||
callbackURL: string,
|
callbackURL: string,
|
||||||
nonce?: string,
|
nonce?: string,
|
||||||
codeChallenge?: string,
|
codeChallenge?: string,
|
||||||
codeChallengeMethod?: string
|
codeChallengeMethod?: string,
|
||||||
|
reauthenticationToken?: string
|
||||||
) {
|
) {
|
||||||
const res = await this.api.post('/oidc/authorize', {
|
const res = await this.api.post('/oidc/authorize', {
|
||||||
scope,
|
scope,
|
||||||
@@ -27,7 +29,8 @@ class OidcService extends APIService {
|
|||||||
callbackURL,
|
callbackURL,
|
||||||
clientId,
|
clientId,
|
||||||
codeChallenge,
|
codeChallenge,
|
||||||
codeChallengeMethod
|
codeChallengeMethod,
|
||||||
|
reauthenticationToken
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.data as AuthorizeResponse;
|
return res.data as AuthorizeResponse;
|
||||||
@@ -65,7 +68,7 @@ class OidcService extends APIService {
|
|||||||
return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
|
return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateClient(id: string, client: OidcClientCreate) {
|
async updateClient(id: string, client: OidcClientUpdate) {
|
||||||
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
|
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,22 +118,16 @@ class OidcService extends APIService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listAuthorizedClients(options?: SearchPaginationSortRequest) {
|
async listOwnAccessibleClients(options?: SearchPaginationSortRequest) {
|
||||||
const res = await this.api.get('/oidc/users/me/clients', {
|
const res = await this.api.get('/oidc/users/me/clients', {
|
||||||
params: options
|
params: options
|
||||||
});
|
});
|
||||||
return res.data as Paginated<AuthorizedOidcClient>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAuthorizedClientsForUser(userId: string, options?: SearchPaginationSortRequest) {
|
return res.data as Paginated<AccessibleOidcClient>;
|
||||||
const res = await this.api.get(`/oidc/users/${userId}/clients`, {
|
|
||||||
params: options
|
|
||||||
});
|
|
||||||
return res.data as Paginated<AuthorizedOidcClient>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeOwnAuthorizedClient(clientId: string) {
|
async revokeOwnAuthorizedClient(clientId: string) {
|
||||||
await this.api.delete(`/oidc/users/me/clients/${clientId}`);
|
await this.api.delete(`/oidc/users/me/authorized-clients/${clientId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,17 +75,17 @@ export default class UserService extends APIService {
|
|||||||
cachedProfilePicture.bustCache(userId);
|
cachedProfilePicture.bustCache(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneTimeAccessToken(expiresAt: Date, userId: string) {
|
async createOneTimeAccessToken(userId: string = 'me', ttl?: string|number) {
|
||||||
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
||||||
userId,
|
userId,
|
||||||
expiresAt
|
ttl,
|
||||||
});
|
});
|
||||||
return res.data.token;
|
return res.data.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSignupToken(expiresAt: Date, usageLimit: number) {
|
async createSignupToken(ttl: string|number, usageLimit: number) {
|
||||||
const res = await this.api.post(`/signup-tokens`, {
|
const res = await this.api.post(`/signup-tokens`, {
|
||||||
expiresAt,
|
ttl,
|
||||||
usageLimit
|
usageLimit
|
||||||
});
|
});
|
||||||
return res.data.token;
|
return res.data.token;
|
||||||
@@ -100,8 +100,8 @@ export default class UserService extends APIService {
|
|||||||
await this.api.post('/one-time-access-email', { email, redirectPath });
|
await this.api.post('/one-time-access-email', { email, redirectPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestOneTimeAccessEmailAsAdmin(userId: string, expiresAt: Date) {
|
async requestOneTimeAccessEmailAsAdmin(userId: string, ttl: string|number) {
|
||||||
await this.api.post(`/users/${userId}/one-time-access-email`, { expiresAt });
|
await this.api.post(`/users/${userId}/one-time-access-email`, { ttl });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserGroups(id: string, userGroupIds: string[]) {
|
async updateUserGroups(id: string, userGroupIds: string[]) {
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ class WebAuthnService extends APIService {
|
|||||||
async updateCredentialName(id: string, name: string) {
|
async updateCredentialName(id: string, name: string) {
|
||||||
await this.api.patch(`/webauthn/credentials/${id}`, { name });
|
await this.api.patch(`/webauthn/credentials/${id}`, { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reauthenticate(body?: AuthenticationResponseJSON) {
|
||||||
|
const res = await this.api.post('/webauthn/reauthenticate', body);
|
||||||
|
return res.data.reauthenticationToken as string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WebAuthnService;
|
export default WebAuthnService;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { writable } from 'svelte/store';
|
|||||||
|
|
||||||
const userStore = writable<User | null>(null);
|
const userStore = writable<User | null>(null);
|
||||||
|
|
||||||
const setUser = (user: User) => {
|
const setUser = async (user: User) => {
|
||||||
if (user.locale) {
|
if (user.locale) {
|
||||||
setLocale(user.locale, false);
|
await setLocale(user.locale, false);
|
||||||
}
|
}
|
||||||
userStore.set(user);
|
userStore.set(user);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CustomClaim } from './custom-claim.type';
|
||||||
|
|
||||||
export type AppConfig = {
|
export type AppConfig = {
|
||||||
appName: string;
|
appName: string;
|
||||||
allowOwnAccountEdit: boolean;
|
allowOwnAccountEdit: boolean;
|
||||||
@@ -14,6 +16,8 @@ export type AllAppConfig = AppConfig & {
|
|||||||
// General
|
// General
|
||||||
sessionDuration: number;
|
sessionDuration: number;
|
||||||
emailsVerified: boolean;
|
emailsVerified: boolean;
|
||||||
|
signupDefaultUserGroupIDs: string[];
|
||||||
|
signupDefaultCustomClaims: CustomClaim[];
|
||||||
// Email
|
// Email
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
smtpPort: number;
|
smtpPort: number;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type OidcClientMetaData = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
hasLogo: boolean;
|
hasLogo: boolean;
|
||||||
|
requiresReauthentication: boolean;
|
||||||
launchURL?: string;
|
launchURL?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export type OidcClient = OidcClientMetaData & {
|
|||||||
logoutCallbackURLs: string[];
|
logoutCallbackURLs: string[];
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
pkceEnabled: boolean;
|
pkceEnabled: boolean;
|
||||||
|
requiresReauthentication: boolean;
|
||||||
credentials?: OidcClientCredentials;
|
credentials?: OidcClientCredentials;
|
||||||
launchURL?: string;
|
launchURL?: string;
|
||||||
};
|
};
|
||||||
@@ -35,7 +37,13 @@ export type OidcClientWithAllowedUserGroupsCount = OidcClient & {
|
|||||||
allowedUserGroupsCount: number;
|
allowedUserGroupsCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
||||||
|
export type OidcClientCreate = OidcClientUpdate & {
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
export type OidcClientUpdateWithLogo = OidcClientUpdate & {
|
||||||
|
logo: File | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
||||||
logo: File | null | undefined;
|
logo: File | null | undefined;
|
||||||
@@ -53,7 +61,6 @@ export type AuthorizeResponse = {
|
|||||||
issuer: string;
|
issuer: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthorizedOidcClient = {
|
export type AccessibleOidcClient = OidcClientMetaData & {
|
||||||
scope: string;
|
lastUsedAt: Date | null;
|
||||||
client: OidcClientMetaData;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
|
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
|
||||||
|
import { setDefaultOptions } from 'date-fns';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
export function setLocale(locale: Locale, reload = true) {
|
export async function setLocale(locale: Locale, reload = true) {
|
||||||
import(`../../../node_modules/zod/v4/locales/${locale}.js`)
|
const [zodResult, dateFnsResult] = await Promise.allSettled([
|
||||||
.then((zodLocale) => z.config(zodLocale.default()))
|
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
|
||||||
.finally(() => {
|
import(`../../../node_modules/date-fns/locale/${locale}.js`)
|
||||||
setParaglideLocale(locale, { reload });
|
]);
|
||||||
|
|
||||||
|
if (zodResult.status === 'fulfilled') {
|
||||||
|
z.config(zodResult.value.default());
|
||||||
|
} else {
|
||||||
|
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
setParaglideLocale(locale, { reload });
|
||||||
|
|
||||||
|
if (dateFnsResult.status === 'fulfilled') {
|
||||||
|
setDefaultOptions({
|
||||||
|
locale: dateFnsResult.value.default
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`Failed to load date-fns locale for ${locale}:`, dateFnsResult.reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import z from 'zod/v4';
|
import z from 'zod/v4';
|
||||||
|
|
||||||
export const optionalString = z
|
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
|
||||||
.string()
|
z.preprocess((v) => (v === '' ? undefined : v), validation);
|
||||||
.transform((v) => (v === '' ? undefined : v))
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
export const optionalUrl = z
|
export const optionalUrl = z
|
||||||
.url()
|
.url()
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
import Header from '$lib/components/header/header.svelte';
|
import Header from '$lib/components/header/header.svelte';
|
||||||
import { Toaster } from '$lib/components/ui/sonner';
|
import { Toaster } from '$lib/components/ui/sonner';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
|
||||||
import userStore from '$lib/stores/user-store';
|
|
||||||
import { getAuthRedirectPath } from '$lib/utils/redirection-util';
|
import { getAuthRedirectPath } from '$lib/utils/redirection-util';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
@@ -28,14 +26,6 @@
|
|||||||
if (redirectPath) {
|
if (redirectPath) {
|
||||||
goto(redirectPath);
|
goto(redirectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
|
||||||
userStore.setUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appConfig) {
|
|
||||||
appConfigStore.set(appConfig);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !appConfig}
|
{#if !appConfig}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
|
import userStore from '$lib/stores/user-store';
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
@@ -19,6 +21,14 @@ export const load: LayoutLoad = async () => {
|
|||||||
|
|
||||||
const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]);
|
const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
await userStore.setUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig) {
|
||||||
|
appConfigStore.set(appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
appConfig
|
appConfig
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import userStore from '$lib/stores/user-store';
|
import userStore from '$lib/stores/user-store';
|
||||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||||
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
|
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
|
||||||
import { startAuthentication } from '@simplewebauthn/browser';
|
import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
let errorMessage: string | null = $state(null);
|
let errorMessage: string | null = $state(null);
|
||||||
let authorizationRequired = $state(false);
|
let authorizationRequired = $state(false);
|
||||||
let authorizationConfirmed = $state(false);
|
let authorizationConfirmed = $state(false);
|
||||||
|
let userSignedInAt: Date | undefined;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($userStore) {
|
if ($userStore) {
|
||||||
@@ -38,13 +39,16 @@
|
|||||||
|
|
||||||
async function authorize() {
|
async function authorize() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
|
let authResponse: AuthenticationResponseJSON | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get access token if not signed in
|
|
||||||
if (!$userStore?.id) {
|
if (!$userStore?.id) {
|
||||||
const loginOptions = await webauthnService.getLoginOptions();
|
const loginOptions = await webauthnService.getLoginOptions();
|
||||||
const authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
||||||
const user = await webauthnService.finishLogin(authResponse);
|
const user = await webauthnService.finishLogin(authResponse);
|
||||||
userStore.setUser(user);
|
userStore.setUser(user);
|
||||||
|
userSignedInAt = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authorizationConfirmed) {
|
if (!authorizationConfirmed) {
|
||||||
@@ -56,8 +60,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reauthToken: string | undefined;
|
||||||
|
if (client?.requiresReauthentication) {
|
||||||
|
let authResponse;
|
||||||
|
const signedInRecently =
|
||||||
|
userSignedInAt && userSignedInAt.getTime() > Date.now() - 60 * 1000;
|
||||||
|
if (!signedInRecently) {
|
||||||
|
const loginOptions = await webauthnService.getLoginOptions();
|
||||||
|
authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
||||||
|
}
|
||||||
|
reauthToken = await webauthnService.reauthenticate(authResponse);
|
||||||
|
}
|
||||||
|
|
||||||
await oidService
|
await oidService
|
||||||
.authorize(client!.id, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod)
|
.authorize(
|
||||||
|
client!.id,
|
||||||
|
scope,
|
||||||
|
callbackURL,
|
||||||
|
nonce,
|
||||||
|
codeChallenge,
|
||||||
|
codeChallengeMethod,
|
||||||
|
reauthToken
|
||||||
|
)
|
||||||
.then(async ({ code, callbackURL, issuer }) => {
|
.then(async ({ code, callbackURL, issuer }) => {
|
||||||
onSuccess(code, callbackURL, issuer);
|
onSuccess(code, callbackURL, issuer);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
const loginOptions = await webauthnService.getLoginOptions();
|
const loginOptions = await webauthnService.getLoginOptions();
|
||||||
const authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
const authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
||||||
const user = await webauthnService.finishLogin(authResponse);
|
const user = await webauthnService.finishLogin(authResponse);
|
||||||
userStore.setUser(user);
|
await userStore.setUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await oidcService.getDeviceCodeInfo(userCode);
|
const info = await oidcService.getDeviceCodeInfo(userCode);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
const authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
const authResponse = await startAuthentication({ optionsJSON: loginOptions });
|
||||||
const user = await webauthnService.finishLogin(authResponse);
|
const user = await webauthnService.finishLogin(authResponse);
|
||||||
|
|
||||||
userStore.setUser(user);
|
await userStore.setUser(user);
|
||||||
goto('/settings');
|
goto('/settings');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = getWebauthnErrorMessage(e);
|
error = getWebauthnErrorMessage(e);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
try {
|
try {
|
||||||
const user = await userService.exchangeOneTimeAccessToken(code);
|
const user = await userService.exchangeOneTimeAccessToken(code);
|
||||||
userStore.setUser(user);
|
await userStore.setUser(user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
goto(data.redirect);
|
goto(data.redirect);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
es: 'Español',
|
es: 'Español',
|
||||||
fr: 'Français',
|
fr: 'Français',
|
||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
|
ko: '한국어',
|
||||||
nl: 'Nederlands',
|
nl: 'Nederlands',
|
||||||
pl: 'Polski',
|
pl: 'Polski',
|
||||||
'pt-BR': 'Português brasileiro',
|
'pt-BR': 'Português brasileiro',
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
...$userStore!,
|
...$userStore!,
|
||||||
locale
|
locale
|
||||||
});
|
});
|
||||||
setLocale(locale);
|
await setLocale(locale);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,8 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
const expiration = new Date(Date.now() + 15 * 60 * 1000);
|
|
||||||
userService
|
userService
|
||||||
.createOneTimeAccessToken(expiration, 'me')
|
.createOneTimeAccessToken('me')
|
||||||
.then((c) => {
|
.then((c) => {
|
||||||
code = c;
|
code = c;
|
||||||
loginCodeLink = page.url.origin + '/lc/' + code;
|
loginCodeLink = page.url.origin + '/lc/' + code;
|
||||||
@@ -52,9 +51,9 @@
|
|||||||
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<CopyToClipboard value={code!}>
|
<CopyToClipboard value={code!}>
|
||||||
<p class="text-3xl font-semibold">{code}</p>
|
<p class="text-3xl font-code">{code}</p>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
|
<div class="flex items-center justify-center gap-3 my-2 text-muted-foreground">
|
||||||
<Separator />
|
<Separator />
|
||||||
<p class="text-xs text-nowrap">{m.or_visit()}</p>
|
<p class="text-xs text-nowrap">{m.or_visit()}</p>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import type { ApiKeyCreate } from '$lib/types/api-key.type';
|
import type { ApiKeyCreate } from '$lib/types/api-key.type';
|
||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { optionalString } from '$lib/utils/zod-util';
|
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(3).max(50),
|
name: z.string().min(3).max(50),
|
||||||
description: optionalString,
|
description: emptyToUndefined(z.string().optional()),
|
||||||
expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future())
|
expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideImage, Mail, SlidersHorizontal, UserSearch } from '@lucide/svelte';
|
import { LucideImage, Mail, SlidersHorizontal, UserSearch, Users } from '@lucide/svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||||
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
||||||
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
||||||
|
import AppConfigSignupDefaultsForm from './forms/app-config-signup-defaults-form.svelte';
|
||||||
import UpdateApplicationImages from './update-application-images.svelte';
|
import UpdateApplicationImages from './update-application-images.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -68,6 +69,17 @@
|
|||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<CollapsibleCard
|
||||||
|
id="application-configuration-signup-defaults"
|
||||||
|
icon={Users}
|
||||||
|
title={m.user_creation()}
|
||||||
|
description={m.configure_user_creation()}
|
||||||
|
>
|
||||||
|
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
||||||
|
</CollapsibleCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<CollapsibleCard
|
<CollapsibleCard
|
||||||
id="application-configuration-email"
|
id="application-configuration-email"
|
||||||
|
|||||||
@@ -23,27 +23,11 @@
|
|||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
|
||||||
const signupOptions = {
|
|
||||||
disabled: {
|
|
||||||
label: m.disabled(),
|
|
||||||
description: m.signup_disabled_description()
|
|
||||||
},
|
|
||||||
withToken: {
|
|
||||||
label: m.signup_with_token(),
|
|
||||||
description: m.signup_with_token_description()
|
|
||||||
},
|
|
||||||
open: {
|
|
||||||
label: m.signup_open(),
|
|
||||||
description: m.signup_open_description()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedAppConfig = {
|
const updatedAppConfig = {
|
||||||
appName: appConfig.appName,
|
appName: appConfig.appName,
|
||||||
sessionDuration: appConfig.sessionDuration,
|
sessionDuration: appConfig.sessionDuration,
|
||||||
emailsVerified: appConfig.emailsVerified,
|
emailsVerified: appConfig.emailsVerified,
|
||||||
allowOwnAccountEdit: appConfig.allowOwnAccountEdit,
|
allowOwnAccountEdit: appConfig.allowOwnAccountEdit,
|
||||||
allowUserSignups: appConfig.allowUserSignups,
|
|
||||||
disableAnimations: appConfig.disableAnimations,
|
disableAnimations: appConfig.disableAnimations,
|
||||||
accentColor: appConfig.accentColor
|
accentColor: appConfig.accentColor
|
||||||
};
|
};
|
||||||
@@ -53,7 +37,6 @@
|
|||||||
sessionDuration: z.number().min(1).max(43200),
|
sessionDuration: z.number().min(1).max(43200),
|
||||||
emailsVerified: z.boolean(),
|
emailsVerified: z.boolean(),
|
||||||
allowOwnAccountEdit: z.boolean(),
|
allowOwnAccountEdit: z.boolean(),
|
||||||
allowUserSignups: z.enum(['disabled', 'withToken', 'open']),
|
|
||||||
disableAnimations: z.boolean(),
|
disableAnimations: z.boolean(),
|
||||||
accentColor: z.string()
|
accentColor: z.string()
|
||||||
});
|
});
|
||||||
@@ -80,55 +63,6 @@
|
|||||||
description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()}
|
description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()}
|
||||||
bind:input={$inputs.sessionDuration}
|
bind:input={$inputs.sessionDuration}
|
||||||
/>
|
/>
|
||||||
<div class="grid gap-2">
|
|
||||||
<div>
|
|
||||||
<Label class="mb-0" for="enable-user-signup">{m.enable_user_signups()}</Label>
|
|
||||||
<p class="text-muted-foreground text-[0.8rem]">
|
|
||||||
{m.enable_user_signups_description()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Select.Root
|
|
||||||
disabled={$appConfigStore.uiConfigDisabled}
|
|
||||||
type="single"
|
|
||||||
value={$inputs.allowUserSignups.value}
|
|
||||||
onValueChange={(v) =>
|
|
||||||
($inputs.allowUserSignups.value = v as typeof $inputs.allowUserSignups.value)}
|
|
||||||
>
|
|
||||||
<Select.Trigger
|
|
||||||
class="w-full"
|
|
||||||
aria-label={m.enable_user_signups()}
|
|
||||||
placeholder={m.enable_user_signups()}
|
|
||||||
>
|
|
||||||
{signupOptions[$inputs.allowUserSignups.value]?.label}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
<Select.Item value="disabled">
|
|
||||||
<div class="flex flex-col items-start gap-1">
|
|
||||||
<span class="font-medium">{signupOptions.disabled.label}</span>
|
|
||||||
<span class="text-muted-foreground text-xs">
|
|
||||||
{signupOptions.disabled.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Select.Item>
|
|
||||||
<Select.Item value="withToken">
|
|
||||||
<div class="flex flex-col items-start gap-1">
|
|
||||||
<span class="font-medium">{signupOptions.withToken.label}</span>
|
|
||||||
<span class="text-muted-foreground text-xs">
|
|
||||||
{signupOptions.withToken.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Select.Item>
|
|
||||||
<Select.Item value="open">
|
|
||||||
<div class="flex flex-col items-start gap-1">
|
|
||||||
<span class="font-medium">{signupOptions.open.label}</span>
|
|
||||||
<span class="text-muted-foreground text-xs">
|
|
||||||
{signupOptions.open.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Select.Item>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
id="self-account-editing"
|
id="self-account-editing"
|
||||||
label={m.enable_self_account_editing()}
|
label={m.enable_self_account_editing()}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||||
|
import SearchableMultiSelect from '$lib/components/form/searchable-multi-select.svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import * as Select from '$lib/components/ui/select';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
|
import { debounced } from '$lib/utils/debounce-util';
|
||||||
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
let {
|
||||||
|
appConfig,
|
||||||
|
callback
|
||||||
|
}: {
|
||||||
|
appConfig: AllAppConfig;
|
||||||
|
callback: (updatedConfig: Partial<AllAppConfig>) => Promise<void>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const userGroupService = new UserGroupService();
|
||||||
|
|
||||||
|
let userGroups = $state<{ value: string; label: string }[]>([]);
|
||||||
|
let selectedGroups = $state<{ value: string; label: string }[]>([]);
|
||||||
|
let customClaims = $state(appConfig.signupDefaultCustomClaims || []);
|
||||||
|
let allowUserSignups = $state(appConfig.allowUserSignups);
|
||||||
|
let isLoading = $state(false);
|
||||||
|
let isUserSearchLoading = $state(false);
|
||||||
|
|
||||||
|
const signupOptions = {
|
||||||
|
disabled: {
|
||||||
|
label: m.disabled(),
|
||||||
|
description: m.signup_disabled_description()
|
||||||
|
},
|
||||||
|
withToken: {
|
||||||
|
label: m.signup_with_token(),
|
||||||
|
description: m.signup_with_token_description()
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
label: m.signup_open(),
|
||||||
|
description: m.signup_open_description()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadUserGroups(search?: string) {
|
||||||
|
userGroups = (await userGroupService.list({ search })).data.map((group) => ({
|
||||||
|
value: group.id,
|
||||||
|
label: group.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Ensure selected groups are still in the list
|
||||||
|
for (const selectedGroup of selectedGroups) {
|
||||||
|
if (!userGroups.some((g) => g.value === selectedGroup.value)) {
|
||||||
|
userGroups.push(selectedGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSelectedGroups() {
|
||||||
|
selectedGroups = (
|
||||||
|
await Promise.all(
|
||||||
|
appConfig.signupDefaultUserGroupIDs.map((groupId) => userGroupService.get(groupId))
|
||||||
|
)
|
||||||
|
).map((group) => ({
|
||||||
|
value: group.id,
|
||||||
|
label: group.name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUserGroupSearch = debounced(
|
||||||
|
async (search: string) => await loadUserGroups(search),
|
||||||
|
300,
|
||||||
|
(loading) => (isUserSearchLoading = loading)
|
||||||
|
);
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
isLoading = true;
|
||||||
|
await callback({
|
||||||
|
allowUserSignups: allowUserSignups,
|
||||||
|
signupDefaultUserGroupIDs: selectedGroups.map((g) => g.value),
|
||||||
|
signupDefaultCustomClaims: customClaims
|
||||||
|
});
|
||||||
|
toast.success(m.user_creation_updated_successfully());
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
loadSelectedGroups();
|
||||||
|
customClaims = appConfig.signupDefaultCustomClaims || [];
|
||||||
|
allowUserSignups = appConfig.allowUserSignups;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => loadUserGroups());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="space-y-6" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div>
|
||||||
|
<Label class="mb-0" for="enable-user-signup">{m.enable_user_signups()}</Label>
|
||||||
|
<p class="text-muted-foreground text-[0.8rem]">
|
||||||
|
{m.enable_user_signups_description()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
value={allowUserSignups}
|
||||||
|
onValueChange={(v) => (allowUserSignups = v as typeof allowUserSignups)}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
id="enable-user-signup"
|
||||||
|
class="w-full"
|
||||||
|
aria-label={m.enable_user_signups()}
|
||||||
|
placeholder={m.enable_user_signups()}
|
||||||
|
>
|
||||||
|
{signupOptions[allowUserSignups]?.label}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="disabled">
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="font-medium">{signupOptions.disabled.label}</span>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{signupOptions.disabled.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Select.Item>
|
||||||
|
<Select.Item value="withToken">
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="font-medium">{signupOptions.withToken.label}</span>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{signupOptions.withToken.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Select.Item>
|
||||||
|
<Select.Item value="open">
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="font-medium">{signupOptions.open.label}</span>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{signupOptions.open.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label for="default-groups" class="mb-0">{m.user_groups()}</Label>
|
||||||
|
<p class="text-muted-foreground mt-1 mb-2 text-xs">
|
||||||
|
{m.user_creation_groups_description()}
|
||||||
|
</p>
|
||||||
|
<SearchableMultiSelect
|
||||||
|
id="default-groups"
|
||||||
|
items={userGroups}
|
||||||
|
oninput={(e) => onUserGroupSearch(e.currentTarget.value)}
|
||||||
|
selectedItems={selectedGroups.map((g) => g.value)}
|
||||||
|
onSelect={(selected) => {
|
||||||
|
selectedGroups = userGroups.filter((g) => selected.includes(g.value));
|
||||||
|
}}
|
||||||
|
isLoading={isUserSearchLoading}
|
||||||
|
disableInternalSearch
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="mb-0">{m.custom_claims()}</Label>
|
||||||
|
<p class="text-muted-foreground mt-1 mb-2 text-xs">
|
||||||
|
{m.user_creation_claims_description()}
|
||||||
|
</p>
|
||||||
|
<CustomClaimsInput bind:customClaims />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-2">
|
||||||
|
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
{#if expandAddClient}
|
{#if expandAddClient}
|
||||||
<div transition:slide>
|
<div transition:slide>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<OIDCClientForm callback={createOIDCClient} />
|
<OIDCClientForm mode="create" callback={createOIDCClient} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
[m.userinfo_url()]: `https://${page.url.hostname}/api/oidc/userinfo`,
|
[m.userinfo_url()]: `https://${page.url.hostname}/api/oidc/userinfo`,
|
||||||
[m.logout_url()]: `https://${page.url.hostname}/api/oidc/end-session`,
|
[m.logout_url()]: `https://${page.url.hostname}/api/oidc/end-session`,
|
||||||
[m.certificate_url()]: `https://${page.url.hostname}/.well-known/jwks.json`,
|
[m.certificate_url()]: `https://${page.url.hostname}/.well-known/jwks.json`,
|
||||||
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled()
|
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled(),
|
||||||
|
[m.requires_reauthentication()]: client.requiresReauthentication ? m.enabled() : m.disabled()
|
||||||
});
|
});
|
||||||
|
|
||||||
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
||||||
@@ -49,6 +50,9 @@
|
|||||||
|
|
||||||
client.isPublic = updatedClient.isPublic;
|
client.isPublic = updatedClient.isPublic;
|
||||||
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
|
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
|
||||||
|
setupDetails[m.requires_reauthentication()] = updatedClient.requiresReauthentication
|
||||||
|
? m.enabled()
|
||||||
|
: m.disabled();
|
||||||
|
|
||||||
await Promise.all([dataPromise, imagePromise])
|
await Promise.all([dataPromise, imagePromise])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -120,14 +124,14 @@
|
|||||||
<Card.Content>
|
<Card.Content>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||||
<Label class="mb-0 w-44">{m.client_id()}</Label>
|
<Label class="mb-0 w-50">{m.client_id()}</Label>
|
||||||
<CopyToClipboard value={client.id}>
|
<CopyToClipboard value={client.id}>
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</div>
|
</div>
|
||||||
{#if !client.isPublic}
|
{#if !client.isPublic}
|
||||||
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||||
<Label class="mb-0 w-44">{m.client_secret()}</Label>
|
<Label class="mb-0 w-50">{m.client_secret()}</Label>
|
||||||
{#if $clientSecretStore}
|
{#if $clientSecretStore}
|
||||||
<CopyToClipboard value={$clientSecretStore}>
|
<CopyToClipboard value={$clientSecretStore}>
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||||
@@ -154,7 +158,7 @@
|
|||||||
<div transition:slide>
|
<div transition:slide>
|
||||||
{#each Object.entries(setupDetails) as [key, value]}
|
{#each Object.entries(setupDetails) as [key, value]}
|
||||||
<div class="mb-5 flex flex-col sm:flex-row sm:items-center">
|
<div class="mb-5 flex flex-col sm:flex-row sm:items-center">
|
||||||
<Label class="mb-0 w-44">{key}</Label>
|
<Label class="mb-0 w-50">{key}</Label>
|
||||||
<CopyToClipboard {value}>
|
<CopyToClipboard {value}>
|
||||||
<span class="text-muted-foreground text-sm">{value}</span>
|
<span class="text-muted-foreground text-sm">{value}</span>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
@@ -175,7 +179,7 @@
|
|||||||
</Card.Root>
|
</Card.Root>
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<OidcForm existingClient={client} callback={updateClient} />
|
<OidcForm mode="update" existingClient={client} callback={updateClient} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
<CollapsibleCard
|
<CollapsibleCard
|
||||||
|
|||||||
@@ -6,24 +6,30 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import type { OidcClient, OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
import type {
|
||||||
|
OidcClient,
|
||||||
|
OidcClientCreateWithLogo,
|
||||||
|
OidcClientUpdateWithLogo
|
||||||
|
} from '$lib/types/oidc.type';
|
||||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { cn } from '$lib/utils/style';
|
import { cn } from '$lib/utils/style';
|
||||||
|
import { emptyToUndefined, optionalUrl } from '$lib/utils/zod-util';
|
||||||
import { LucideChevronDown } from '@lucide/svelte';
|
import { LucideChevronDown } from '@lucide/svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import FederatedIdentitiesInput from './federated-identities-input.svelte';
|
import FederatedIdentitiesInput from './federated-identities-input.svelte';
|
||||||
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
||||||
import { optionalUrl } from '$lib/utils/zod-util';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
callback,
|
callback,
|
||||||
existingClient
|
existingClient,
|
||||||
|
mode
|
||||||
}: {
|
}: {
|
||||||
existingClient?: OidcClient;
|
existingClient?: OidcClient;
|
||||||
callback: (user: OidcClientCreateWithLogo) => Promise<boolean>;
|
callback: (client: OidcClientCreateWithLogo | OidcClientUpdateWithLogo) => Promise<boolean>;
|
||||||
|
mode: 'create' | 'update';
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
@@ -34,11 +40,13 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
|
id: '',
|
||||||
name: existingClient?.name || '',
|
name: existingClient?.name || '',
|
||||||
callbackURLs: existingClient?.callbackURLs || [],
|
callbackURLs: existingClient?.callbackURLs || [],
|
||||||
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
|
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
|
||||||
isPublic: existingClient?.isPublic || false,
|
isPublic: existingClient?.isPublic || false,
|
||||||
pkceEnabled: existingClient?.pkceEnabled || false,
|
pkceEnabled: existingClient?.pkceEnabled || false,
|
||||||
|
requiresReauthentication: existingClient?.requiresReauthentication || false,
|
||||||
launchURL: existingClient?.launchURL || '',
|
launchURL: existingClient?.launchURL || '',
|
||||||
credentials: {
|
credentials: {
|
||||||
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
|
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
|
||||||
@@ -46,11 +54,22 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
id: emptyToUndefined(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.min(2)
|
||||||
|
.max(128)
|
||||||
|
.regex(/^[a-zA-Z0-9_-]+$/, {
|
||||||
|
message: m.invalid_client_id()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
),
|
||||||
name: z.string().min(2).max(50),
|
name: z.string().min(2).max(50),
|
||||||
callbackURLs: z.array(z.string().nonempty()).default([]),
|
callbackURLs: z.array(z.string().nonempty()).default([]),
|
||||||
logoutCallbackURLs: z.array(z.string().nonempty()),
|
logoutCallbackURLs: z.array(z.string().nonempty()),
|
||||||
isPublic: z.boolean(),
|
isPublic: z.boolean(),
|
||||||
pkceEnabled: z.boolean(),
|
pkceEnabled: z.boolean(),
|
||||||
|
requiresReauthentication: z.boolean(),
|
||||||
launchURL: optionalUrl,
|
launchURL: optionalUrl,
|
||||||
credentials: z.object({
|
credentials: z.object({
|
||||||
federatedIdentities: z.array(
|
federatedIdentities: z.array(
|
||||||
@@ -147,6 +166,12 @@
|
|||||||
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
|
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
|
||||||
bind:checked={$inputs.pkceEnabled.value}
|
bind:checked={$inputs.pkceEnabled.value}
|
||||||
/>
|
/>
|
||||||
|
<SwitchWithLabel
|
||||||
|
id="requires-reauthentication"
|
||||||
|
label={m.requires_reauthentication()}
|
||||||
|
description={m.requires_users_to_authenticate_again_on_each_authorization()}
|
||||||
|
bind:checked={$inputs.requiresReauthentication.value}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<Label for="logo">{m.logo()}</Label>
|
<Label for="logo">{m.logo()}</Label>
|
||||||
@@ -177,7 +202,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showAdvancedOptions}
|
{#if showAdvancedOptions}
|
||||||
<div class="mt-5 md:col-span-2" transition:slide={{ duration: 200 }}>
|
<div class="mt-7 flex flex-col gap-y-7 md:col-span-2" transition:slide={{ duration: 200 }}>
|
||||||
|
{#if mode == 'create'}
|
||||||
|
<FormInput
|
||||||
|
label={m.client_id()}
|
||||||
|
placeholder={m.generated()}
|
||||||
|
class="w-full md:w-1/2"
|
||||||
|
description={m.custom_client_id_description()}
|
||||||
|
bind:input={$inputs.id}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<FederatedIdentitiesInput
|
<FederatedIdentitiesInput
|
||||||
client={existingClient}
|
client={existingClient}
|
||||||
bind:federatedIdentities={$inputs.credentials.value.federatedIdentities}
|
bind:federatedIdentities={$inputs.credentials.value.federatedIdentities}
|
||||||
@@ -189,7 +223,7 @@
|
|||||||
<div class="relative mt-5 flex justify-center">
|
<div class="relative mt-5 flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="text-muted-foregroun"
|
class="text-muted-foreground"
|
||||||
onclick={() => (showAdvancedOptions = !showAdvancedOptions)}
|
onclick={() => (showAdvancedOptions = !showAdvancedOptions)}
|
||||||
>
|
>
|
||||||
{showAdvancedOptions ? m.hide_advanced_options() : m.show_advanced_options()}
|
{showAdvancedOptions ? m.hide_advanced_options() : m.show_advanced_options()}
|
||||||
|
|||||||
@@ -3,25 +3,25 @@
|
|||||||
import * as Pagination from '$lib/components/ui/pagination';
|
import * as Pagination from '$lib/components/ui/pagination';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import OIDCService from '$lib/services/oidc-service';
|
import OIDCService from '$lib/services/oidc-service';
|
||||||
import type { AuthorizedOidcClient, OidcClientMetaData } from '$lib/types/oidc.type';
|
import type { AccessibleOidcClient, OidcClientMetaData } from '$lib/types/oidc.type';
|
||||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LayoutDashboard } from '@lucide/svelte';
|
import { LayoutDashboard } from '@lucide/svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { default as AuthorizedOidcClientCard } from './authorized-oidc-client-card.svelte';
|
import AuthorizedOidcClientCard from './authorized-oidc-client-card.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let authorizedClients: Paginated<AuthorizedOidcClient> = $state(data.authorizedClients);
|
let clients: Paginated<AccessibleOidcClient> = $state(data.clients);
|
||||||
let requestOptions: SearchPaginationSortRequest = $state(data.appRequestOptions);
|
let requestOptions: SearchPaginationSortRequest = $state(data.appRequestOptions);
|
||||||
|
|
||||||
const oidcService = new OIDCService();
|
const oidcService = new OIDCService();
|
||||||
|
|
||||||
async function onRefresh(options: SearchPaginationSortRequest) {
|
async function onRefresh(options: SearchPaginationSortRequest) {
|
||||||
authorizedClients = await oidcService.listAuthorizedClients(options);
|
clients = await oidcService.listOwnAccessibleClients(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPageChange(page: number) {
|
async function onPageChange(page: number) {
|
||||||
requestOptions.pagination = { limit: authorizedClients.pagination.itemsPerPage, page };
|
requestOptions.pagination = { limit: clients.pagination.itemsPerPage, page };
|
||||||
onRefresh(requestOptions);
|
onRefresh(requestOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if authorizedClients.data.length === 0}
|
{#if clients.data.length === 0}
|
||||||
<div class="py-16 text-center">
|
<div class="py-16 text-center">
|
||||||
<LayoutDashboard class="text-muted-foreground mx-auto mb-4 size-16" />
|
<LayoutDashboard class="text-muted-foreground mx-auto mb-4 size-16" />
|
||||||
<h3 class="text-muted-foreground mb-2 text-lg font-medium">
|
<h3 class="text-muted-foreground mb-2 text-lg font-medium">
|
||||||
@@ -76,20 +76,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<div
|
||||||
{#each authorizedClients.data as authorizedClient}
|
class="grid gap-3"
|
||||||
<AuthorizedOidcClientCard {authorizedClient} onRevoke={revokeAuthorizedClient} />
|
style="grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));"
|
||||||
|
>
|
||||||
|
{#each clients.data as client}
|
||||||
|
<AuthorizedOidcClientCard {client} onRevoke={revokeAuthorizedClient} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if authorizedClients.pagination.totalPages > 1}
|
{#if clients.pagination.totalPages > 1}
|
||||||
<div class="border-border flex items-center justify-center border-t pt-3">
|
<div class="border-border flex items-center justify-center border-t pt-3">
|
||||||
<Pagination.Root
|
<Pagination.Root
|
||||||
class="mx-0 w-auto"
|
class="mx-0 w-auto"
|
||||||
count={authorizedClients.pagination.totalItems}
|
count={clients.pagination.totalItems}
|
||||||
perPage={authorizedClients.pagination.itemsPerPage}
|
perPage={clients.pagination.itemsPerPage}
|
||||||
{onPageChange}
|
{onPageChange}
|
||||||
page={authorizedClients.pagination.currentPage}
|
page={clients.pagination.currentPage}
|
||||||
>
|
>
|
||||||
{#snippet children({ pages })}
|
{#snippet children({ pages })}
|
||||||
<Pagination.Content class="flex justify-center">
|
<Pagination.Content class="flex justify-center">
|
||||||
@@ -101,7 +104,7 @@
|
|||||||
<Pagination.Item>
|
<Pagination.Item>
|
||||||
<Pagination.Link
|
<Pagination.Link
|
||||||
{page}
|
{page}
|
||||||
isActive={authorizedClients.pagination.currentPage === page.value}
|
isActive={clients.pagination.currentPage === page.value}
|
||||||
>
|
>
|
||||||
{page.value}
|
{page.value}
|
||||||
</Pagination.Link>
|
</Pagination.Link>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const load: PageLoad = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizedClients = await oidcService.listAuthorizedClients(appRequestOptions);
|
const clients = await oidcService.listOwnAccessibleClients(appRequestOptions);
|
||||||
|
|
||||||
return { authorizedClients, appRequestOptions };
|
return { clients, appRequestOptions };
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user