Compare commits

...

34 Commits

Author SHA1 Message Date
Elias Schneider
dc5968cd30 release: 0.53.0 2025-05-08 21:56:49 +02:00
Elias Schneider
63a0c08696 fix: handle CORS correctly for endpoints that SPAs need (#513) 2025-05-08 21:56:17 +02:00
Elias Schneider
6c415e7769 chore(translations): update translations via Crowdin (#517) 2025-05-08 20:48:45 +02:00
Elias Schneider
90bdd29fb6 ci/cd: add explicit permissions to actions 2025-05-07 16:48:18 +02:00
Elias Schneider
e0db4695ac refactor: run formatter 2025-05-07 16:43:24 +02:00
Elias Schneider
de648dd6da ci/cd: remove wait for LDAP sync 2025-05-07 16:40:10 +02:00
Kyle Mendell
73c82ae43a tests: add e2e LDAP tests (#466)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-05-07 14:38:02 +00:00
Elias Schneider
ba256c76bc refactor: organize imports 2025-05-07 09:58:38 +02:00
Elias Schneider
5e2e947fe0 feat: add support for TZ environment variable 2025-05-07 09:55:30 +02:00
Elias Schneider
f4281e4f69 release: 0.52.0 2025-05-06 22:14:39 +02:00
Alessandro (Ale) Segala
3c87e4ec14 feat: add healthz endpoint (#494) 2025-05-06 22:14:18 +02:00
Elias Schneider
c55fef057c fix: correctly set script permissions inside Docker container 2025-05-06 21:18:45 +02:00
Daenney
6f54ee5d66 feat: OpenTelemetry tracing and metrics (#262) (#495)
Co-authored-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
2025-05-05 15:59:44 +02:00
github-actions[bot]
9efab5f3e8 chore: update AAGUIDs (#507)
Co-authored-by: stonith404 <58886915+stonith404@users.noreply.github.com>
2025-05-05 15:38:26 +02:00
Elias Schneider
364f5b38b9 ci/cd: create a PR instead of commiting for update aaguids workflow 2025-05-05 09:38:55 +02:00
Kyle Mendell
5d78445501 ci/cd: build frontend to include paraglide before running svelte-check 2025-05-04 10:08:01 -05:00
Kyle Mendell
8ec2388269 ci/cd: add svelte-check workflow for the frontend 2025-05-03 21:48:25 -05:00
Elias Schneider
dbacdb5bf0 release: 0.51.1 2025-05-03 23:42:47 +02:00
Elias Schneider
f4c6cff461 refactor: fix type errors 2025-05-03 23:42:17 +02:00
Elias Schneider
0b9cbf47e3 fix: allow LDAP users to update their locale 2025-05-03 23:32:56 +02:00
Alessandro (Ale) Segala
bda178c2bb refactor: complete graceful shutdown implementation and add service runner (#493) 2025-05-03 23:25:22 +02:00
Elias Schneider
6bd6cefaa6 fix: non admin users weren't able to call the end session endpoint 2025-05-03 22:53:55 +02:00
dependabot[bot]
83be1e0b49 chore(deps-dev): bump vite from 6.2.6 to 6.3.4 in /frontend in the npm_and_yarn group across 1 directory (#496)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 14:53:30 -05:00
Kyle Mendell
cf3fe0be84 fix: last name still showing as required on account form (#492)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
2025-04-29 20:24:16 +02:00
Elias Schneider
ec76e1c111 chore(translations): update translations via Crowdin (#491) 2025-04-29 00:04:12 -05:00
Elias Schneider
6004f84845 release: 0.51.0 2025-04-28 11:15:52 +02:00
Alessandro (Ale) Segala
3ec98736cf refactor: graceful shutdown for server (#482) 2025-04-28 11:13:50 +02:00
Elias Schneider
ce24372c57 fix: do not require PKCE for public clients 2025-04-28 11:02:35 +02:00
Elias Schneider
4614769b84 refactor: reorganize imports 2025-04-28 10:49:54 +02:00
Elias Schneider
86d2b5f59f fix: return correct error message if user isn't authorized 2025-04-28 10:39:17 +02:00
Elias Schneider
1efd1d182d fix: hide global audit log switch for non admin users 2025-04-28 10:38:53 +02:00
Elias Schneider
0a24ab8001 fix: updating scopes of an authorized client fails with Postgres 2025-04-28 09:29:18 +02:00
James18232
02cacba5c5 feat: new login code card position for mobile devices (#452)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
2025-04-28 04:04:48 +00:00
Elias Schneider
38653e2aa4 chore(translations): update translations via Crowdin (#485) 2025-04-27 23:00:37 -05:00
171 changed files with 8291 additions and 7010 deletions

21
.github/svelte-check-matcher.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
"problemMatcher": [
{
"owner": "svelte-check",
"pattern": [
{
"regexp": "^([^\\s].*):(\\d+):(\\d+)$",
"file": 1,
"line": 2,
"column": 3
},
{
"regexp": "^\\s*(Error|Warning):\\s*(.*)\\s+\\((?:ts|js|svelte)\\)$",
"severity": 1,
"message": 2,
"loop": false
}
]
}
]
}

View File

@@ -17,6 +17,9 @@ jobs:
build:
if: github.event.pull_request.head.ref != 'i18n_crowdin'
timeout-minutes: 20
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -44,6 +47,9 @@ jobs:
test-sqlite:
if: github.event.pull_request.head.ref != 'i18n_crowdin'
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
needs: build
steps:
@@ -72,6 +78,23 @@ jobs:
- 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 frontend dependencies
working-directory: ./frontend
run: npm ci
@@ -81,9 +104,18 @@ jobs:
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Run Docker Container with Sqlite DB
- name: Create Docker network
run: docker network create pocket-id-network
- name: Setup and Configure LLDAP Server
run: |
chmod +x ./scripts/tests/setup-lldap.sh
./scripts/tests/setup-lldap.sh
- name: Run Docker Container with Sqlite DB and LDAP
run: |
docker run -d --name pocket-id-sqlite \
--network pocket-id-network \
-p 80:80 \
-e APP_ENV=test \
pocket-id:test
@@ -114,6 +146,9 @@ jobs:
test-postgres:
if: github.event.pull_request.head.ref != 'i18n_crowdin'
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
needs: build
steps:
@@ -150,6 +185,23 @@ jobs:
if: steps.postgres-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/postgres-image.tar
- name: Cache LLDAP Docker image
uses: actions/cache@v3
id: lldap-cache
with:
path: /tmp/lldap-image.tar
key: lldap-stable-${{ runner.os }}
- name: Pull and save LLDAP image
if: steps.lldap-cache.outputs.cache-hit != 'true'
run: |
docker pull nitnelave/lldap:stable
docker save nitnelave/lldap:stable > /tmp/lldap-image.tar
- name: Load LLDAP image from cache
if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
@@ -181,9 +233,14 @@ jobs:
-p 5432:5432 \
postgres:17
- name: Setup and Configure LLDAP Server
run: |
chmod +x ./scripts/tests/setup-lldap.sh
./scripts/tests/setup-lldap.sh
- name: Wait for Postgres to start
run: |
for i in {1..10}; do
for i in {1..5}; do
if docker exec pocket-id-db pg_isready -U postgres; then
echo "Postgres is ready"
break
@@ -192,7 +249,7 @@ jobs:
sleep 2
done
- name: Run Docker Container with Postgres DB
- name: Run Docker Container with Postgres DB and LDAP
run: |
docker run -d --name pocket-id-postgres \
--network pocket-id-network \

59
.github/workflows/svelte-check.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Svelte Check
on:
push:
branches: [main]
paths:
- "frontend/src/**"
- ".github/svelte-check-matcher.json"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/tsconfig.json"
- "frontend/svelte.config.js"
pull_request:
branches: [main]
paths:
- "frontend/src/**"
- ".github/svelte-check-matcher.json"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/tsconfig.json"
- "frontend/svelte.config.js"
workflow_dispatch:
jobs:
type-check:
name: Run Svelte Check
# Don't run on dependabot branches
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Build Pocket ID Frontend
working-directory: frontend
run: npm run build
- name: Add svelte-check problem matcher
run: echo "::add-matcher::.github/svelte-check-matcher.json"
- name: Run svelte-check
working-directory: frontend
run: npm run check

View File

@@ -11,13 +11,16 @@ on:
jobs:
test-backend:
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'backend/go.mod'
cache-dependency-path: 'backend/go.sum'
go-version-file: "backend/go.mod"
cache-dependency-path: "backend/go.sum"
- name: Install dependencies
working-directory: backend
run: |

View File

@@ -5,9 +5,10 @@ on:
- cron: "0 0 * * 1" # Runs every Monday at midnight
workflow_dispatch: # Allows manual triggering of the workflow
permissions:
contents: write
permissions:
contents: write
pull-requests: write
jobs:
update-aaguids:
runs-on: ubuntu-latest
@@ -25,10 +26,13 @@ jobs:
mkdir -p backend/resources
jq -c 'map_values(.name)' data.json > backend/resources/aaguids.json
- name: Commit changes
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/resources/aaguids.json
git diff --staged --quiet || git commit -m "chore: update AAGUIDs"
git push
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
commit-message: "chore: update AAGUIDs"
title: "chore: update AAGUIDs"
body: |
This PR updates the AAGUIDs file with the latest data from the [passkey-aaguids](https://github.com/pocket-id/passkey-aaguids) repository.
branch: update-aaguids
base: main
delete-branch: true

View File

@@ -1 +1 @@
0.50.0
0.53.0

View File

@@ -1,3 +1,52 @@
## [](https://github.com/pocket-id/pocket-id/compare/v0.52.0...v) (2025-05-08)
### Features
* add support for `TZ` environment variable ([5e2e947](https://github.com/pocket-id/pocket-id/commit/5e2e947fe09fa881a7bbc70133a243a4baf30e90))
### Bug Fixes
* handle CORS correctly for endpoints that SPAs need ([#513](https://github.com/pocket-id/pocket-id/issues/513)) ([63a0c08](https://github.com/pocket-id/pocket-id/commit/63a0c08696938e1cefd12018f4bd38aa1808996a))
## [](https://github.com/pocket-id/pocket-id/compare/v0.51.1...v) (2025-05-06)
### Features
* add healthz endpoint ([#494](https://github.com/pocket-id/pocket-id/issues/494)) ([3c87e4e](https://github.com/pocket-id/pocket-id/commit/3c87e4ec1468c314ac7f8fe831e97b5eead88112))
* OpenTelemetry tracing and metrics ([#262](https://github.com/pocket-id/pocket-id/issues/262)) ([#495](https://github.com/pocket-id/pocket-id/issues/495)) ([6f54ee5](https://github.com/pocket-id/pocket-id/commit/6f54ee5d668d7a26911db10f2402daf6a1f75f68))
### Bug Fixes
* correctly set script permissions inside Docker container ([c55fef0](https://github.com/pocket-id/pocket-id/commit/c55fef057cdcec867af91b29968541983cd80ec0))
## [](https://github.com/pocket-id/pocket-id/compare/v0.51.0...v) (2025-05-03)
### Bug Fixes
* allow LDAP users to update their locale ([0b9cbf4](https://github.com/pocket-id/pocket-id/commit/0b9cbf47e36a332cfd854aa92e761264fb3e4795))
* last name still showing as required on account form ([#492](https://github.com/pocket-id/pocket-id/issues/492)) ([cf3fe0b](https://github.com/pocket-id/pocket-id/commit/cf3fe0be84f6365f5d4eb08c1b47905962a48a0d))
* non admin users weren't able to call the end session endpoint ([6bd6cef](https://github.com/pocket-id/pocket-id/commit/6bd6cefaa6dc571a319a6a1c2b2facc2404eadd3))
## [](https://github.com/pocket-id/pocket-id/compare/v0.50.0...v) (2025-04-28)
### Features
* new login code card position for mobile devices ([#452](https://github.com/pocket-id/pocket-id/issues/452)) ([02cacba](https://github.com/pocket-id/pocket-id/commit/02cacba5c5524481684cb0e1790811df113a9481))
### Bug Fixes
* do not require PKCE for public clients ([ce24372](https://github.com/pocket-id/pocket-id/commit/ce24372c571cc3b277095dc6a4107663d64f45b3))
* hide global audit log switch for non admin users ([1efd1d1](https://github.com/pocket-id/pocket-id/commit/1efd1d182dbb6190d3c7e27034426c9e48781b4a))
* return correct error message if user isn't authorized ([86d2b5f](https://github.com/pocket-id/pocket-id/commit/86d2b5f59f26cb944017826cbd8df915cdc986f1))
* updating scopes of an authorized client fails with Postgres ([0a24ab8](https://github.com/pocket-id/pocket-id/commit/0a24ab80010eb5a15d99915802c6698274a5c57c))
## [](https://github.com/pocket-id/pocket-id/compare/v0.49.0...v) (2025-04-27)

View File

@@ -1,5 +1,6 @@
# Tags passed to "go build"
ARG BUILD_TAGS=""
ARG VERSION="unknown"
# Stage 1: Build Frontend
FROM node:22-alpine AS frontend-builder
@@ -25,6 +26,7 @@ RUN CGO_ENABLED=1 \
GOOS=linux \
go build \
-tags "${BUILD_TAGS}" \
-ldflags="-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${VERSION}" \
-o /app/backend/pocket-id-backend \
.
@@ -44,7 +46,7 @@ COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
COPY ./scripts ./scripts
RUN chmod +x ./scripts/**/*.sh
RUN find ./scripts -name "*.sh" -exec chmod +x {} \;
EXPOSE 80
ENV APP_ENV=production

View File

@@ -1,6 +1,10 @@
package main
import (
"log"
_ "time/tzdata"
"github.com/pocket-id/pocket-id/backend/internal/bootstrap"
)
@@ -9,5 +13,8 @@ import (
// @description.markdown
func main() {
bootstrap.Bootstrap()
err := bootstrap.Bootstrap()
if err != nil {
log.Fatal(err.Error())
}
}

View File

@@ -12,7 +12,7 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/go-co-op/gocron/v2 v2.15.0
github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-playground/validator/v10 v10.24.0
github.com/go-playground/validator/v10 v10.25.0
github.com/go-webauthn/webauthn v0.11.2
github.com/golang-migrate/migrate/v4 v4.18.2
github.com/google/uuid v1.6.0
@@ -20,7 +20,16 @@ require (
github.com/lestrrat-go/jwx/v3 v3.0.0-beta1
github.com/mileusna/useragent v1.3.5
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/metric v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/sdk/metric v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.24.0
golang.org/x/time v0.9.0
@@ -31,21 +40,28 @@ require (
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/bytedance/sonic v1.12.8 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.10 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/disintegration/gift v1.1.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-webauthn/x v0.1.16 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -56,8 +72,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -69,20 +84,43 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
go.opentelemetry.io/otel/log v0.10.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.10.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -6,17 +6,22 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI=
github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -58,6 +63,7 @@ github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawr
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -68,22 +74,25 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8=
github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -91,6 +100,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -129,14 +140,18 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
@@ -170,6 +185,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -178,16 +195,22 @@ github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2 h1:jG+FaCBv3h6GD5F+oenTfe3
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -211,20 +234,60 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 h1:HY2hJ7yn3KuEBBBsKxvF3ViSmzLwsgeNvD+0utRMgzc=
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0/go.mod h1:H4H7vs8766kwFnOZVEGMJFVF+phpBSmTckvvNRdJeDI=
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA=
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0/go.mod h1:fPl+qlrhRdRntIpPs9JoQ0iBKAsnH5VkgppU1f9kyF4=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
go.opentelemetry.io/otel/log v0.10.0 h1:1CXmspaRITvFcjA4kyVszuG4HjA61fPDxMb7q3BuyF0=
go.opentelemetry.io/otel/log v0.10.0/go.mod h1:PbVdm9bXKku/gL0oFfUF4wwsQsOPlpo4VEqjvxih+FM=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw=
go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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=
@@ -309,8 +372,14 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -38,7 +38,6 @@ func initApplicationImages() {
log.Fatalf("Error copying file: %v", err)
}
}
}
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
@@ -55,6 +54,11 @@ func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
}
func getImageNameWithoutExtension(fileName string) string {
splitted := strings.Split(fileName, ".")
return strings.Join(splitted[:len(splitted)-1], ".")
idx := strings.LastIndexByte(fileName, '.')
if idx < 1 {
// No dot found, or fileName starts with a dot
return fileName
}
return fileName[:idx]
}

View File

@@ -2,23 +2,75 @@ package bootstrap
import (
"context"
"fmt"
"log"
"time"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/job"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/internal/utils/signals"
)
func Bootstrap() {
ctx := context.TODO()
func Bootstrap() error {
// Get a context that is canceled when the application is stopping
ctx := signals.SignalContext(context.Background())
initApplicationImages()
// Perform migrations for changes
migrateConfigDBConnstring()
db := newDatabase()
appConfigService := service.NewAppConfigService(ctx, db)
migrateKey()
initRouter(ctx, db, appConfigService)
// Initialize the tracer and metrics exporter
shutdownFns, httpClient, err := initOtel(ctx, common.EnvConfig.MetricsEnabled, common.EnvConfig.TracingEnabled)
if err != nil {
return fmt.Errorf("failed to initialize OpenTelemetry: %w", err)
}
// Connect to the database
db := newDatabase()
// Create all services
svc, err := initServices(ctx, db, httpClient)
if err != nil {
return fmt.Errorf("failed to initialize services: %w", err)
}
// Init the job scheduler
scheduler, err := job.NewScheduler()
if err != nil {
return fmt.Errorf("failed to create job scheduler: %w", err)
}
err = registerScheduledJobs(ctx, db, svc, scheduler)
if err != nil {
return fmt.Errorf("failed to register scheduled jobs: %w", err)
}
// Init the router
router := initRouter(db, svc)
// Run all background serivces
// This call blocks until the context is canceled
err = utils.
NewServiceRunner(router, scheduler.Run).
Run(ctx)
if err != nil {
return fmt.Errorf("failed to run services: %w", err)
}
// Invoke all shutdown functions
// We give these a timeout of 5s
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
err = utils.
NewServiceRunner(shutdownFns...).
Run(shutdownCtx)
if err != nil {
log.Printf("Error shutting down services: %v", err)
}
return nil
}

View File

@@ -12,9 +12,9 @@ import (
// When building for E2E tests, add the e2etest controller
func init() {
registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService){
func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService) {
testService := service.NewTestService(db, appConfigService, jwtService)
registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){
func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
testService := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService)
controller.NewTestController(apiGroup, testService)
},
}

View File

@@ -0,0 +1,107 @@
package bootstrap
import (
"context"
"fmt"
"net/http"
"time"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
metricnoop "go.opentelemetry.io/otel/metric/noop"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
tracenoop "go.opentelemetry.io/otel/trace/noop"
)
func defaultResource() (*resource.Resource, error) {
return resource.Merge(
resource.Default(),
resource.NewSchemaless(
semconv.ServiceName("pocket-id-backend"),
semconv.ServiceVersion(common.Version),
),
)
}
func initOtel(ctx context.Context, metrics, traces bool) (shutdownFns []utils.Service, httpClient *http.Client, err error) {
resource, err := defaultResource()
if err != nil {
return nil, nil, fmt.Errorf("failed to create OpenTelemetry resource: %w", err)
}
shutdownFns = make([]utils.Service, 0, 2)
httpClient = &http.Client{}
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
if !ok {
// Indicates a development-time error
panic("Default transport is not of type *http.Transport")
}
httpClient.Transport = defaultTransport.Clone()
if traces {
tr, err := autoexport.NewSpanExporter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize OpenTelemetry span exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(resource),
sdktrace.WithBatcher(tr),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
),
)
shutdownFns = append(shutdownFns, func(shutdownCtx context.Context) error { //nolint:contextcheck
tpCtx, tpCancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer tpCancel()
shutdownErr := tp.Shutdown(tpCtx)
if shutdownErr != nil {
return fmt.Errorf("failed to gracefully shut down traces exporter: %w", shutdownErr)
}
return nil
})
httpClient.Transport = otelhttp.NewTransport(httpClient.Transport)
} else {
otel.SetTracerProvider(tracenoop.NewTracerProvider())
}
if metrics {
mr, err := autoexport.NewMetricReader(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize OpenTelemetry metric reader: %w", err)
}
mp := metric.NewMeterProvider(
metric.WithResource(resource),
metric.WithReader(mr),
)
otel.SetMeterProvider(mp)
shutdownFns = append(shutdownFns, func(shutdownCtx context.Context) error { //nolint:contextcheck
mpCtx, mpCancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer mpCancel()
shutdownErr := mp.Shutdown(mpCtx)
if shutdownErr != nil {
return fmt.Errorf("failed to gracefully shut down metrics exporter: %w", shutdownErr)
}
return nil
})
} else {
otel.SetMeterProvider(metricnoop.NewMeterProvider())
}
return shutdownFns, httpClient, nil
}

View File

@@ -2,25 +2,36 @@ package bootstrap
import (
"context"
"fmt"
"log"
"net"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/controller"
"github.com/pocket-id/pocket-id/backend/internal/job"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/utils/systemd"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"golang.org/x/time/rate"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/controller"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/internal/utils/systemd"
)
// This is used to register additional controllers for tests
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService)
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services)
func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) {
func initRouter(db *gorm.DB, svc *services) utils.Service {
runner, err := initRouterInternal(db, svc)
if err != nil {
log.Fatalf("failed to init router: %v", err)
}
return runner
}
func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
// Set the appropriate Gin mode based on the environment
switch common.EnvConfig.AppEnv {
case "production":
@@ -34,75 +45,97 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
r := gin.Default()
r.Use(gin.Logger())
// Initialize services
emailService, err := service.NewEmailService(appConfigService, db)
if err != nil {
log.Fatalf("Unable to create email service: %v", err)
if common.EnvConfig.TracingEnabled {
r.Use(otelgin.Middleware("pocket-id-backend"))
}
geoLiteService := service.NewGeoLiteService(ctx)
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
jwtService := service.NewJwtService(appConfigService)
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
userService := service.NewUserService(db, jwtService, auditLogService, emailService, appConfigService)
customClaimService := service.NewCustomClaimService(db)
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
userGroupService := service.NewUserGroupService(db, appConfigService)
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
apiKeyService := service.NewApiKeyService(db, emailService)
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
rateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60)
// Setup global middleware
r.Use(middleware.NewCorsMiddleware().Add())
r.Use(middleware.NewErrorHandlerMiddleware().Add())
r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60))
job.RegisterLdapJobs(ctx, ldapService, appConfigService)
job.RegisterDbCleanupJobs(ctx, db)
job.RegisterFileCleanupJobs(ctx, db)
job.RegisterApiKeyExpiryJob(ctx, apiKeyService, appConfigService)
// Initialize middleware for specific routes
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService)
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
// Set up API routes
apiGroup := r.Group("/api")
controller.NewApiKeyController(apiGroup, authMiddleware, apiKeyService)
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService)
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, oidcService, jwtService)
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService)
controller.NewAppConfigController(apiGroup, authMiddleware, appConfigService, emailService, ldapService)
controller.NewAuditLogController(apiGroup, auditLogService, authMiddleware)
controller.NewUserGroupController(apiGroup, authMiddleware, userGroupService)
controller.NewCustomClaimController(apiGroup, authMiddleware, customClaimService)
apiGroup := r.Group("/api", rateLimitMiddleware)
controller.NewApiKeyController(apiGroup, authMiddleware, svc.apiKeyService)
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.webauthnService, svc.appConfigService)
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService)
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.appConfigService)
controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService)
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
controller.NewUserGroupController(apiGroup, authMiddleware, svc.userGroupService)
controller.NewCustomClaimController(apiGroup, authMiddleware, svc.customClaimService)
// Add test controller in non-production environments
if common.EnvConfig.AppEnv != "production" {
for _, f := range registerTestControllers {
f(apiGroup, db, appConfigService, jwtService)
f(apiGroup, db, svc)
}
}
// Set up base routes
baseGroup := r.Group("/")
controller.NewWellKnownController(baseGroup, jwtService)
baseGroup := r.Group("/", rateLimitMiddleware)
controller.NewWellKnownController(baseGroup, svc.jwtService)
// Get the listener
l, err := net.Listen("tcp", common.EnvConfig.Host+":"+common.EnvConfig.Port)
// Set up healthcheck routes
// These are not rate-limited
controller.NewHealthzController(r)
// Set up the server
srv := &http.Server{
Addr: net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port),
MaxHeaderBytes: 1 << 20,
ReadHeaderTimeout: 10 * time.Second,
Handler: r,
}
// Set up the listener
listener, err := net.Listen("tcp", srv.Addr)
if err != nil {
log.Fatal(err)
return nil, fmt.Errorf("failed to create TCP listener: %w", err)
}
// Notify systemd that we are ready
if err := systemd.SdNotifyReady(); err != nil {
log.Println("Unable to notify systemd that the service is ready: ", err)
// continue to serve anyway since it's not that important
// Service runner function
runFn := func(ctx context.Context) error {
log.Printf("Server listening on %s", srv.Addr)
// Start the server in a background goroutine
go func() {
defer listener.Close()
// Next call blocks until the server is shut down
srvErr := srv.Serve(listener)
if srvErr != http.ErrServerClosed {
log.Fatalf("Error starting app server: %v", srvErr)
}
}()
// Notify systemd that we are ready
err = systemd.SdNotifyReady()
if err != nil {
// Log the error only
log.Printf("[WARN] Unable to notify systemd that the service is ready: %v", err)
}
// Block until the context is canceled
<-ctx.Done()
// Handle graceful shutdown
// Note we use the background context here as ctx has been canceled already
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck
shutdownCancel()
if shutdownErr != nil {
// Log the error only (could be context canceled)
log.Printf("[WARN] App server shutdown error: %v", shutdownErr)
}
return nil
}
// Serve requests
if err := r.RunListener(l); err != nil {
log.Fatal(err)
}
return runFn, nil
}

View File

@@ -0,0 +1,35 @@
package bootstrap
import (
"context"
"fmt"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/job"
)
func registerScheduledJobs(ctx context.Context, db *gorm.DB, svc *services, scheduler *job.Scheduler) error {
err := scheduler.RegisterLdapJobs(ctx, svc.ldapService, svc.appConfigService)
if err != nil {
return fmt.Errorf("failed to register LDAP jobs in scheduler: %w", err)
}
err = scheduler.RegisterGeoLiteUpdateJobs(ctx, svc.geoLiteService)
if err != nil {
return fmt.Errorf("failed to register GeoLite DB update service: %w", err)
}
err = scheduler.RegisterDbCleanupJobs(ctx, db)
if err != nil {
return fmt.Errorf("failed to register DB cleanup jobs in scheduler: %w", err)
}
err = scheduler.RegisterFileCleanupJobs(ctx, db)
if err != nil {
return fmt.Errorf("failed to register file cleanup jobs in scheduler: %w", err)
}
err = scheduler.RegisterApiKeyExpiryJob(ctx, svc.apiKeyService, svc.appConfigService)
if err != nil {
return fmt.Errorf("failed to register API key expiration jobs in scheduler: %w", err)
}
return nil
}

View File

@@ -0,0 +1,52 @@
package bootstrap
import (
"context"
"fmt"
"net/http"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
type services struct {
appConfigService *service.AppConfigService
emailService *service.EmailService
geoLiteService *service.GeoLiteService
auditLogService *service.AuditLogService
jwtService *service.JwtService
webauthnService *service.WebAuthnService
userService *service.UserService
customClaimService *service.CustomClaimService
oidcService *service.OidcService
userGroupService *service.UserGroupService
ldapService *service.LdapService
apiKeyService *service.ApiKeyService
}
// Initializes all services
// The context should be used by services only for initialization, and not for running
func initServices(initCtx context.Context, db *gorm.DB, httpClient *http.Client) (svc *services, err error) {
svc = &services{}
svc.appConfigService = service.NewAppConfigService(initCtx, db)
svc.emailService, err = service.NewEmailService(db, svc.appConfigService)
if err != nil {
return nil, fmt.Errorf("unable to create email service: %w", err)
}
svc.geoLiteService = service.NewGeoLiteService(httpClient)
svc.auditLogService = service.NewAuditLogService(db, svc.appConfigService, svc.emailService, svc.geoLiteService)
svc.jwtService = service.NewJwtService(svc.appConfigService)
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
svc.customClaimService = service.NewCustomClaimService(db)
svc.oidcService = service.NewOidcService(db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService)
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 = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
return svc, nil
}

View File

@@ -10,6 +10,13 @@ import (
type DbProvider string
const (
// TracerName should be passed to otel.Tracer, trace.SpanFromContext when creating custom spans.
TracerName = "github.com/pocket-id/pocket-id/backend/tracing"
// MeterName should be passed to otel.Meter when create custom metrics.
MeterName = "github.com/pocket-id/pocket-id/backend/metrics"
)
const (
DbProviderSqlite DbProvider = "sqlite"
DbProviderPostgres DbProvider = "postgres"
@@ -31,6 +38,8 @@ type EnvConfigSchema struct {
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
UiConfigDisabled bool `env:"PUBLIC_UI_CONFIG_DISABLED"`
MetricsEnabled bool `env:"METRICS_ENABLED"`
TracingEnabled bool `env:"TRACING_ENABLED"`
}
var EnvConfig = &EnvConfigSchema{
@@ -48,6 +57,8 @@ var EnvConfig = &EnvConfigSchema{
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
GeoLiteDBUrl: MaxMindGeoLiteCityUrl,
UiConfigDisabled: false,
MetricsEnabled: false,
TracingEnabled: false,
}
func init() {

View File

@@ -0,0 +1,6 @@
package common
// Version contains the Pocket ID version.
//
// It can be set at build time using -ldflags.
var Version = "unknown"

View File

@@ -41,6 +41,16 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
return
}
if err := tc.TestService.SetLdapTestConfig(c.Request.Context()); err != nil {
_ = c.Error(err)
return
}
if err := tc.TestService.SyncLdap(c.Request.Context()); err != nil {
_ = c.Error(err)
return
}
tc.TestService.SetJWTKeys()
c.Status(http.StatusNoContent)

View File

@@ -0,0 +1,29 @@
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
)
// NewHealthzController creates a new controller for the healthcheck endpoints
// @Summary Healthcheck controller
// @Description Initializes healthcheck endpoints
// @Tags Health
func NewHealthzController(r *gin.Engine) {
hc := &HealthzController{}
r.GET("/healthz", hc.healthzHandler)
}
type HealthzController struct{}
// healthzHandler godoc
// @Summary Responds to healthchecks
// @Description Responds with a successful status code to healthcheck requests
// @Tags Health
// @Success 204 ""
// @Router /healthz [get]
func (hc *HealthzController) healthzHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}

View File

@@ -30,8 +30,8 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
group.POST("/oidc/token", oc.createTokensHandler)
group.GET("/oidc/userinfo", oc.userInfoHandler)
group.POST("/oidc/userinfo", oc.userInfoHandler)
group.POST("/oidc/end-session", authMiddleware.WithSuccessOptional().Add(), oc.EndSessionHandler)
group.GET("/oidc/end-session", authMiddleware.WithSuccessOptional().Add(), oc.EndSessionHandler)
group.POST("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
group.GET("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
group.POST("/oidc/introspect", oc.introspectTokenHandler)
group.GET("/oidc/clients", authMiddleware.Add(), oc.listClientsHandler)
@@ -129,9 +129,6 @@ func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Contex
// @Success 200 {object} dto.OidcTokenResponseDto "Token response with access_token and optional id_token and refresh_token"
// @Router /api/oidc/token [post]
func (oc *OidcController) createTokensHandler(c *gin.Context) {
// Disable cors for this endpoint
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
var input dto.OidcCreateTokensDto
if err := c.ShouldBind(&input); err != nil {
_ = c.Error(err)

View File

@@ -4,7 +4,6 @@ import (
"context"
"log"
"github.com/go-co-op/gocron/v2"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
@@ -13,20 +12,13 @@ type ApiKeyEmailJobs struct {
appConfigService *service.AppConfigService
}
func RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *service.ApiKeyService, appConfigService *service.AppConfigService) {
func (s *Scheduler) RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *service.ApiKeyService, appConfigService *service.AppConfigService) error {
jobs := &ApiKeyEmailJobs{
apiKeyService: apiKeyService,
appConfigService: appConfigService,
}
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("Failed to create a new scheduler: %v", err)
}
registerJob(ctx, scheduler, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
scheduler.Start()
return s.registerJob(ctx, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
}
func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error {

View File

@@ -2,30 +2,25 @@ package job
import (
"context"
"log"
"errors"
"time"
"github.com/go-co-op/gocron/v2"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
)
func RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) {
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("Failed to create a new scheduler: %s", err)
}
func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) error {
jobs := &DbCleanupJobs{db: db}
registerJob(ctx, scheduler, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions)
registerJob(ctx, scheduler, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens)
registerJob(ctx, scheduler, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes)
registerJob(ctx, scheduler, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens)
registerJob(ctx, scheduler, "ClearAuditLogs", "0 3 * * *", jobs.clearAuditLogs)
scheduler.Start()
return errors.Join(
s.registerJob(ctx, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions),
s.registerJob(ctx, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens),
s.registerJob(ctx, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes),
s.registerJob(ctx, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens),
s.registerJob(ctx, "ClearAuditLogs", "0 3 * * *", jobs.clearAuditLogs),
)
}
type DbCleanupJobs struct {

View File

@@ -8,24 +8,16 @@ import (
"path/filepath"
"strings"
"github.com/go-co-op/gocron/v2"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model"
)
func RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) {
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("Failed to create a new scheduler: %s", err)
}
func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) error {
jobs := &FileCleanupJobs{db: db}
registerJob(ctx, scheduler, "ClearUnusedDefaultProfilePictures", "0 2 * * 0", jobs.clearUnusedDefaultProfilePictures)
scheduler.Start()
return s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", "0 2 * * 0", jobs.clearUnusedDefaultProfilePictures)
}
type FileCleanupJobs struct {

View File

@@ -0,0 +1,45 @@
package job
import (
"context"
"log"
"time"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
type GeoLiteUpdateJobs struct {
geoLiteService *service.GeoLiteService
}
func (s *Scheduler) RegisterGeoLiteUpdateJobs(ctx context.Context, geoLiteService *service.GeoLiteService) error {
// Check if the service needs periodic updating
if geoLiteService.DisableUpdater() {
// Nothing to do
return nil
}
jobs := &GeoLiteUpdateJobs{geoLiteService: geoLiteService}
// Register the job to run every day, at 5 minutes past midnight
err := s.registerJob(ctx, "UpdateGeoLiteDB", "5 * */1 * *", jobs.updateGoeLiteDB)
if err != nil {
return err
}
// Run the job immediately on startup, with a 1s delay
go func() {
time.Sleep(time.Second)
err = jobs.updateGoeLiteDB(ctx)
if err != nil {
// Log the error only, but don't return it
log.Printf("Failed to Update GeoLite database: %v", err)
}
}()
return nil
}
func (j *GeoLiteUpdateJobs) updateGoeLiteDB(ctx context.Context) error {
return j.geoLiteService.UpdateDatabase(ctx)
}

View File

@@ -1,29 +0,0 @@
package job
import (
"context"
"log"
"github.com/go-co-op/gocron/v2"
"github.com/google/uuid"
)
func registerJob(ctx context.Context, scheduler gocron.Scheduler, name string, interval string, job func(ctx context.Context) error) {
_, err := scheduler.NewJob(
gocron.CronJob(interval, false),
gocron.NewTask(job),
gocron.WithContext(ctx),
gocron.WithEventListeners(
gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
log.Printf("Job %q run successfully", name)
}),
gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
log.Printf("Job %q failed with error: %v", name, err)
}),
),
)
if err != nil {
log.Fatalf("Failed to register job %q: %v", name, err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"log"
"github.com/go-co-op/gocron/v2"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
@@ -13,24 +12,23 @@ type LdapJobs struct {
appConfigService *service.AppConfigService
}
func RegisterLdapJobs(ctx context.Context, ldapService *service.LdapService, appConfigService *service.AppConfigService) {
func (s *Scheduler) RegisterLdapJobs(ctx context.Context, ldapService *service.LdapService, appConfigService *service.AppConfigService) error {
jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService}
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("Failed to create a new scheduler: %v", err)
}
// Register the job to run every hour
registerJob(ctx, scheduler, "SyncLdap", "0 * * * *", jobs.syncLdap)
err := s.registerJob(ctx, "SyncLdap", "0 * * * *", jobs.syncLdap)
if err != nil {
return err
}
// Run the job immediately on startup
err = jobs.syncLdap(ctx)
if err != nil {
// Log the error only, but don't return it
log.Printf("Failed to sync LDAP: %v", err)
}
scheduler.Start()
return nil
}
func (j *LdapJobs) syncLdap(ctx context.Context) error {

View File

@@ -0,0 +1,66 @@
package job
import (
"context"
"fmt"
"log"
"github.com/go-co-op/gocron/v2"
"github.com/google/uuid"
)
type Scheduler struct {
scheduler gocron.Scheduler
}
func NewScheduler() (*Scheduler, error) {
scheduler, err := gocron.NewScheduler()
if err != nil {
return nil, fmt.Errorf("failed to create a new scheduler: %w", err)
}
return &Scheduler{
scheduler: scheduler,
}, nil
}
// Run the scheduler.
// This function blocks until the context is canceled.
func (s *Scheduler) Run(ctx context.Context) error {
log.Println("Starting job scheduler")
s.scheduler.Start()
// Block until context is canceled
<-ctx.Done()
err := s.scheduler.Shutdown()
if err != nil {
log.Printf("[WARN] Error shutting down job scheduler: %v", err)
} else {
log.Println("Job scheduler shut down")
}
return nil
}
func (s *Scheduler) registerJob(ctx context.Context, name string, interval string, job func(ctx context.Context) error) error {
_, err := s.scheduler.NewJob(
gocron.CronJob(interval, false),
gocron.NewTask(job),
gocron.WithContext(ctx),
gocron.WithEventListeners(
gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
log.Printf("Job %q run successfully", name)
}),
gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
log.Printf("Job %q failed with error: %v", name, err)
}),
),
)
if err != nil {
return fmt.Errorf("failed to register job %q: %w", name, err)
}
return nil
}

View File

@@ -1,7 +1,10 @@
package middleware
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
@@ -69,6 +72,13 @@ func (m *AuthMiddleware) Add() gin.HandlerFunc {
return
}
// If JWT auth failed and the error is not a NotSignedInError, abort the request
if !errors.Is(err, &common.NotSignedInError{}) {
c.Abort()
_ = c.Error(err)
return
}
// JWT auth failed, try API key auth
userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired)
if err == nil {

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
)
type CorsMiddleware struct{}
@@ -15,17 +14,21 @@ func NewCorsMiddleware() *CorsMiddleware {
func (m *CorsMiddleware) Add() gin.HandlerFunc {
return func(c *gin.Context) {
// Allow all origins for the token endpoint
switch c.FullPath() {
case "/api/oidc/token", "/api/oidc/introspect":
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
default:
c.Writer.Header().Set("Access-Control-Allow-Origin", common.EnvConfig.AppURL)
path := c.FullPath()
if path == "" {
// The router doesn't map preflight requests, so we need to use the raw URL path
path = c.Request.URL.Path
}
c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if !isCorsPath(path) {
c.Next()
return
}
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
// Preflight request
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(204)
return
@@ -34,3 +37,17 @@ func (m *CorsMiddleware) Add() gin.HandlerFunc {
c.Next()
}
}
func isCorsPath(path string) bool {
switch path {
case "/api/oidc/token",
"/api/oidc/userinfo",
"/oidc/end-session",
"/api/oidc/introspect",
"/.well-known/jwks.json",
"/.well-known/openid-configuration":
return true
default:
return false
}
}

View File

@@ -26,12 +26,12 @@ type AppConfigService struct {
db *gorm.DB
}
func NewAppConfigService(ctx context.Context, db *gorm.DB) *AppConfigService {
func NewAppConfigService(initCtx context.Context, db *gorm.DB) *AppConfigService {
service := &AppConfigService{
db: db,
}
err := service.LoadDbConfig(ctx)
err := service.LoadDbConfig(initCtx)
if err != nil {
log.Fatalf("Failed to initialize app config service: %v", err)
}

View File

@@ -29,15 +29,16 @@ type TestService struct {
db *gorm.DB
jwtService *JwtService
appConfigService *AppConfigService
ldapService *LdapService
}
func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService) *TestService {
return &TestService{db: db, appConfigService: appConfigService, jwtService: jwtService}
func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService) *TestService {
return &TestService{db: db, appConfigService: appConfigService, jwtService: jwtService, ldapService: ldapService}
}
//nolint:gocognit
func (s *TestService) SeedDatabase() error {
return s.db.Transaction(func(tx *gorm.DB) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
users := []model.User{
{
Base: model.Base{
@@ -238,6 +239,12 @@ func (s *TestService) SeedDatabase() error {
return nil
})
if err != nil {
return err
}
return nil
}
func (s *TestService) ResetDatabase() error {
@@ -349,3 +356,52 @@ func (s *TestService) getCborPublicKey(base64PublicKey string) ([]byte, error) {
return cborPublicKey, nil
}
// SyncLdap triggers an LDAP synchronization
func (s *TestService) SyncLdap(ctx context.Context) error {
return s.ldapService.SyncAll(ctx)
}
// SetLdapTestConfig writes the test LDAP config variables directly to the database.
func (s *TestService) SetLdapTestConfig(ctx context.Context) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
ldapConfigs := map[string]string{
"ldapUrl": "ldap://lldap:3890",
"ldapBindDn": "uid=admin,ou=people,dc=pocket-id,dc=org",
"ldapBindPassword": "admin_password",
"ldapBase": "dc=pocket-id,dc=org",
"ldapUserSearchFilter": "(objectClass=person)",
"ldapUserGroupSearchFilter": "(objectClass=groupOfNames)",
"ldapSkipCertVerify": "true",
"ldapAttributeUserUniqueIdentifier": "uuid",
"ldapAttributeUserUsername": "uid",
"ldapAttributeUserEmail": "mail",
"ldapAttributeUserFirstName": "givenName",
"ldapAttributeUserLastName": "sn",
"ldapAttributeGroupUniqueIdentifier": "uuid",
"ldapAttributeGroupName": "uid",
"ldapAttributeGroupMember": "member",
"ldapAttributeAdminGroup": "admin_group",
"ldapSoftDeleteUsers": "true",
"ldapEnabled": "true",
}
for key, value := range ldapConfigs {
configVar := model.AppConfigVariable{Key: key, Value: value}
if err := tx.Create(&configVar).Error; err != nil {
return fmt.Errorf("failed to create config variable '%s': %w", key, err)
}
}
return nil
})
if err != nil {
return fmt.Errorf("failed to set LDAP test config: %w", err)
}
if err := s.appConfigService.LoadDbConfig(ctx); err != nil {
return fmt.Errorf("failed to load app config: %w", err)
}
return nil
}

View File

@@ -33,7 +33,7 @@ type EmailService struct {
textTemplates map[string]*ttemplate.Template
}
func NewEmailService(appConfigService *AppConfigService, db *gorm.DB) (*EmailService, error) {
func NewEmailService(db *gorm.DB, appConfigService *AppConfigService) (*EmailService, error) {
htmlTemplates, err := email.PrepareHTMLTemplates(emailTemplatesPaths)
if err != nil {
return nil, fmt.Errorf("prepare html templates: %w", err)

View File

@@ -22,8 +22,9 @@ import (
)
type GeoLiteService struct {
httpClient *http.Client
disableUpdater bool
mutex sync.Mutex
mutex sync.RWMutex
}
var localhostIPNets = []*net.IPNet{
@@ -42,25 +43,24 @@ var tailscaleIPNets = []*net.IPNet{
}
// NewGeoLiteService initializes a new GeoLiteService instance and starts a goroutine to update the GeoLite2 City database.
func NewGeoLiteService(ctx context.Context) *GeoLiteService {
service := &GeoLiteService{}
func NewGeoLiteService(httpClient *http.Client) *GeoLiteService {
service := &GeoLiteService{
httpClient: httpClient,
}
if common.EnvConfig.MaxMindLicenseKey == "" && common.EnvConfig.GeoLiteDBUrl == common.MaxMindGeoLiteCityUrl {
// Warn the user, and disable the updater.
// Warn the user, and disable the periodic updater
log.Println("MAXMIND_LICENSE_KEY environment variable is empty. The GeoLite2 City database won't be updated.")
service.disableUpdater = true
}
go func() {
err := service.updateDatabase(ctx)
if err != nil {
log.Printf("Failed to update GeoLite2 City database: %v", err)
}
}()
return service
}
func (s *GeoLiteService) DisableUpdater() bool {
return s.disableUpdater
}
// GetLocationByIP returns the country and city of the given IP address.
func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string, err error) {
// Check the IP address against known private IP ranges
@@ -83,8 +83,8 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
}
// Race condition between reading and writing the database.
s.mutex.Lock()
defer s.mutex.Unlock()
s.mutex.RLock()
defer s.mutex.RUnlock()
db, err := maxminddb.Open(common.EnvConfig.GeoLiteDBPath)
if err != nil {
@@ -92,7 +92,10 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
}
defer db.Close()
addr := netip.MustParseAddr(ipAddress)
addr, err := netip.ParseAddr(ipAddress)
if err != nil {
return "", "", fmt.Errorf("failed to parse IP address: %w", err)
}
var record struct {
City struct {
@@ -112,18 +115,13 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
}
// UpdateDatabase checks the age of the database and updates it if it's older than 14 days.
func (s *GeoLiteService) updateDatabase(parentCtx context.Context) error {
if s.disableUpdater {
// Avoid updating the GeoLite2 City database.
return nil
}
func (s *GeoLiteService) UpdateDatabase(parentCtx context.Context) error {
if s.isDatabaseUpToDate() {
log.Println("GeoLite2 City database is up-to-date.")
log.Println("GeoLite2 City database is up-to-date")
return nil
}
log.Println("Updating GeoLite2 City database...")
log.Println("Updating GeoLite2 City database")
downloadUrl := fmt.Sprintf(common.EnvConfig.GeoLiteDBUrl, common.EnvConfig.MaxMindLicenseKey)
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Minute)
@@ -134,7 +132,7 @@ func (s *GeoLiteService) updateDatabase(parentCtx context.Context) error {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to download database: %w", err)
}
@@ -145,7 +143,8 @@ func (s *GeoLiteService) updateDatabase(parentCtx context.Context) error {
}
// Extract the database file directly to the target path
if err := s.extractDatabase(resp.Body); err != nil {
err = s.extractDatabase(resp.Body)
if err != nil {
return fmt.Errorf("failed to extract database: %w", err)
}
@@ -179,10 +178,9 @@ func (s *GeoLiteService) extractDatabase(reader io.Reader) error {
// Iterate over the files in the tar archive
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
} else if err != nil {
return fmt.Errorf("failed to read tar archive: %w", err)
}

View File

@@ -23,14 +23,16 @@ import (
type LdapService struct {
db *gorm.DB
httpClient *http.Client
appConfigService *AppConfigService
userService *UserService
groupService *UserGroupService
}
func NewLdapService(db *gorm.DB, appConfigService *AppConfigService, userService *UserService, groupService *UserGroupService) *LdapService {
func NewLdapService(db *gorm.DB, httpClient *http.Client, appConfigService *AppConfigService, userService *UserService, groupService *UserGroupService) *LdapService {
return &LdapService{
db: db,
httpClient: httpClient,
appConfigService: appConfigService,
userService: userService,
groupService: groupService,
@@ -393,7 +395,7 @@ func (s *LdapService) saveProfilePicture(parentCtx context.Context, userId strin
_, err := url.ParseRequestURI(pictureString)
if err == nil {
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second)
defer cancel()
var req *http.Request
@@ -403,7 +405,7 @@ func (s *LdapService) saveProfilePicture(parentCtx context.Context, userId strin
}
var res *http.Response
res, err = http.DefaultClient.Do(req)
res, err = s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to download profile picture: %w", err)
}

View File

@@ -15,6 +15,8 @@ import (
"strings"
"time"
"gorm.io/gorm/clause"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/pocket-id/pocket-id/backend/internal/common"
@@ -94,24 +96,8 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
// If the user has not authorized the client, create a new authorization in the database
if !hasAuthorizedClient {
userAuthorizedClient := model.UserAuthorizedOidcClient{
UserID: userID,
ClientID: input.ClientID,
Scope: input.Scope,
}
err = tx.
WithContext(ctx).
Create(&userAuthorizedClient).
Error
if errors.Is(err, gorm.ErrDuplicatedKey) {
// The client has already been authorized but with a different scope so we need to update the scope
if err := tx.
WithContext(ctx).
Model(&userAuthorizedClient).Update("scope", input.Scope).Error; err != nil {
return "", "", err
}
} else if err != nil {
err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
if err != nil {
return "", "", err
}
}
@@ -201,7 +187,7 @@ func (s *OidcService) createTokenFromDeviceCode(ctx context.Context, deviceCode,
tx.Rollback()
}()
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, tx)
_, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
if err != nil {
return "", "", "", 0, err
}
@@ -269,7 +255,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, code
tx.Rollback()
}()
client, err := s.VerifyClientCredentials(ctx, clientID, clientSecret, tx)
client, err := s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
if err != nil {
return "", "", "", 0, err
}
@@ -342,7 +328,7 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, refreshTo
tx.Rollback()
}()
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, tx)
_, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
if err != nil {
return "", "", 0, err
}
@@ -401,7 +387,7 @@ func (s *OidcService) IntrospectToken(ctx context.Context, clientID, clientSecre
return introspectDto, &common.OidcMissingClientCredentialsError{}
}
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, s.db)
_, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, s.db)
if err != nil {
return introspectDto, err
}
@@ -520,7 +506,7 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
LogoutCallbackURLs: input.LogoutCallbackURLs,
CreatedByID: userID,
IsPublic: input.IsPublic,
PkceEnabled: input.IsPublic || input.PkceEnabled,
PkceEnabled: input.PkceEnabled,
}
err := s.db.
@@ -999,7 +985,7 @@ func (s *OidcService) getCallbackURL(urls []string, inputCallbackURL string) (ca
}
func (s *OidcService) CreateDeviceAuthorization(ctx context.Context, input dto.OidcDeviceAuthorizationRequestDto) (*dto.OidcDeviceAuthorizationResponseDto, error) {
client, err := s.VerifyClientCredentials(ctx, input.ClientID, input.ClientSecret, s.db)
client, err := s.verifyClientCredentialsInternal(ctx, input.ClientID, input.ClientSecret, s.db)
if err != nil {
return nil, err
}
@@ -1095,23 +1081,11 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
}
if !hasAuthorizedClient {
userAuthorizedClient := model.UserAuthorizedOidcClient{
UserID: userID,
ClientID: deviceAuth.ClientID,
Scope: deviceAuth.Scope,
err := s.createAuthorizedClientInternal(ctx, userID, deviceAuth.ClientID, deviceAuth.Scope, tx)
if err != nil {
return err
}
if err := tx.WithContext(ctx).Create(&userAuthorizedClient).Error; err != nil {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return err
}
// If duplicate, update scope
if err := tx.WithContext(ctx).Model(&model.UserAuthorizedOidcClient{}).
Where("user_id = ? AND client_id = ?", userID, deviceAuth.ClientID).
Update("scope", deviceAuth.Scope).Error; err != nil {
return err
}
}
s.auditLogService.Create(ctx, model.AuditLogEventNewDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
} else {
s.auditLogService.Create(ctx, model.AuditLogEventDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
@@ -1188,7 +1162,25 @@ func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, u
return refreshToken, nil
}
func (s *OidcService) VerifyClientCredentials(ctx context.Context, clientID, clientSecret string, tx *gorm.DB) (model.OidcClient, error) {
func (s *OidcService) createAuthorizedClientInternal(ctx context.Context, userID string, clientID string, scope string, tx *gorm.DB) error {
userAuthorizedClient := model.UserAuthorizedOidcClient{
UserID: userID,
ClientID: clientID,
Scope: scope,
}
err := tx.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "client_id"}},
DoUpdates: clause.AssignmentColumns([]string{"scope"}),
}).
Create(&userAuthorizedClient).
Error
return err
}
func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, clientID, clientSecret string, tx *gorm.DB) (model.OidcClient, error) {
if clientID == "" {
return model.OidcClient{}, &common.OidcMissingClientCredentialsError{}
}

View File

@@ -262,13 +262,13 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
return user, nil
}
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, allowLdapUpdate 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()
defer func() {
tx.Rollback()
}()
user, err := s.updateUserInternal(ctx, userID, updatedUser, updateOwnUser, allowLdapUpdate, tx)
user, err := s.updateUserInternal(ctx, userID, updatedUser, updateOwnUser, isLdapSync, tx)
if err != nil {
return model.User{}, err
}
@@ -292,19 +292,23 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
return model.User{}, err
}
// Disallow updating the user if it is an LDAP group and LDAP is enabled
if !isLdapSync && user.LdapID != nil && s.appConfigService.GetDbConfig().LdapEnabled.IsTrue() {
return model.User{}, &common.LdapUserUpdateError{}
}
// Check if this is an LDAP user and LDAP is enabled
isLdapUser := user.LdapID != nil && s.appConfigService.GetDbConfig().LdapEnabled.IsTrue()
user.FirstName = updatedUser.FirstName
user.LastName = updatedUser.LastName
user.Email = updatedUser.Email
user.Username = updatedUser.Username
user.Locale = updatedUser.Locale
if !updateOwnUser {
user.IsAdmin = updatedUser.IsAdmin
user.Disabled = updatedUser.Disabled
// For LDAP users, only allow updating the locale unless it's an LDAP sync
if !isLdapSync && isLdapUser {
// Only update the locale for LDAP users
user.Locale = updatedUser.Locale
} else {
user.FirstName = updatedUser.FirstName
user.LastName = updatedUser.LastName
user.Email = updatedUser.Email
user.Username = updatedUser.Username
user.Locale = updatedUser.Locale
if !updateOwnUser {
user.IsAdmin = updatedUser.IsAdmin
user.Disabled = updatedUser.Disabled
}
}
err = tx.

View File

@@ -0,0 +1,58 @@
package utils
import (
"context"
"errors"
)
// Source:
// https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/pkg/utils/servicerunner.go
// Copyright (c) 2018, Thom Seddon & Contributors Copyright (c) 2023, Alessandro Segala & Contributors
// License: MIT (https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/LICENSE.md)
// Service is a background service
type Service func(ctx context.Context) error
// ServiceRunner oversees a number of services running in background
type ServiceRunner struct {
services []Service
}
// NewServiceRunner creates a new ServiceRunner
func NewServiceRunner(services ...Service) *ServiceRunner {
return &ServiceRunner{
services: services,
}
}
// Run all background services
func (r *ServiceRunner) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errCh := make(chan error)
for _, service := range r.services {
go func(service Service) {
// Run the service
rErr := service(ctx)
// Ignore context canceled errors here as they generally indicate that the service is stopping
if rErr != nil && !errors.Is(rErr, context.Canceled) {
errCh <- rErr
return
}
errCh <- nil
}(service)
}
// Wait for all services to return
errs := make([]error, 0)
for range len(r.services) {
err := <-errCh
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}

View File

@@ -0,0 +1,125 @@
package utils
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// Source:
// https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/pkg/utils/servicerunner.go
// Copyright (c) 2018, Thom Seddon & Contributors Copyright (c) 2023, Alessandro Segala & Contributors
// License: MIT (https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/LICENSE.md)
func TestServiceRunner_Run(t *testing.T) {
t.Run("successful services", func(t *testing.T) {
// Create a service that just returns no error after 0.2s
successService := func(ctx context.Context) error {
time.Sleep(200 * time.Millisecond)
return nil
}
// Create a service runner with two success services
runner := NewServiceRunner(successService, successService)
// Run the services with a timeout to avoid hanging if something goes wrong
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
// Run should return nil when all services succeed
err := runner.Run(ctx)
require.NoError(t, err)
})
t.Run("service with error", func(t *testing.T) {
// Create a service that returns an error
expectedErr := errors.New("service failed")
errorService := func(ctx context.Context) error {
return expectedErr
}
// Create a service runner with one error service and one success service
successService := func(ctx context.Context) error {
time.Sleep(200 * time.Millisecond)
return nil
}
runner := NewServiceRunner(errorService, successService)
// Run the services with a timeout
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
// Run should return the error
err := runner.Run(ctx)
require.Error(t, err)
// The error should contain our expected error
require.ErrorIs(t, err, expectedErr)
})
t.Run("context canceled", func(t *testing.T) {
// Create a service that waits until context is canceled
waitingService := func(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
}
// Create another service that returns no error quickly
quickService := func(ctx context.Context) error {
return nil
}
runner := NewServiceRunner(waitingService, quickService)
// Create a context that we can cancel
ctx, cancel := context.WithCancel(t.Context())
// Run the runner in a goroutine
errCh := make(chan error)
go func() {
errCh <- runner.Run(ctx)
}()
// Cancel the context to trigger service shutdown
cancel()
// Wait for the runner to finish with a timeout
select {
case err := <-errCh:
require.NoError(t, err, "expected nil error (context.Canceled should be ignored)")
case <-time.After(5 * time.Second):
t.Fatal("test timed out waiting for runner to finish")
}
})
t.Run("multiple errors", func(t *testing.T) {
// Create two services that return different errors
err1 := errors.New("error 1")
err2 := errors.New("error 2")
service1 := func(ctx context.Context) error {
return err1
}
service2 := func(ctx context.Context) error {
return err2
}
runner := NewServiceRunner(service1, service2)
// Run the services
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
// Run should join all errors
err := runner.Run(ctx)
require.Error(t, err)
// Check that both errors are included
require.ErrorIs(t, err, err1)
require.ErrorIs(t, err, err2)
})
}

View File

@@ -0,0 +1,40 @@
package signals
import (
"context"
"log"
"os"
"os/signal"
"syscall"
)
/*
This code is adapted from:
https://github.com/kubernetes-sigs/controller-runtime/blob/8499b67e316a03b260c73f92d0380de8cd2e97a1/pkg/manager/signals/signal.go
Copyright 2017 The Kubernetes Authors.
License: Apache2 (https://github.com/kubernetes-sigs/controller-runtime/blob/8499b67e316a03b260c73f92d0380de8cd2e97a1/LICENSE)
*/
var onlyOneSignalHandler = make(chan struct{})
// SignalContext returns a context that is canceled when the application receives an interrupt signal.
// A second signal forces an immediate shutdown.
func SignalContext(parentCtx context.Context) context.Context {
close(onlyOneSignalHandler) // Panics when called twice
ctx, cancel := context.WithCancel(parentCtx)
sigCh := make(chan os.Signal, 2)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("Received interrupt signal. Shutting down…")
cancel()
<-sigCh
log.Println("Received a second interrupt signal. Forcing an immediate shutdown.")
os.Exit(1)
}()
return ctx
}

File diff suppressed because one or more lines are too long

1
data.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,9 +7,9 @@ services:
- 3000:80
volumes:
- "./data:/app/backend/data"
# Optional healthcheck
# Optional healthcheck
healthcheck:
test: "curl -f http://localhost/health"
test: "curl -f http://localhost/healthz"
interval: 1m30s
timeout: 5s
retries: 2

View File

@@ -276,7 +276,7 @@
"callback_urls": "URL zpětného volání",
"logout_callback_urls": "URL zpětného volání při odhlášení",
"public_client": "Veřejný klient",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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",
"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ů.",
"name_logo": "Logo {name}",
@@ -342,5 +342,9 @@
"show_code": "Zobrazit kód",
"callback_url_description": "URL poskytnuté klientem. Klientské zástupné znaky (*) jsou podporovány, ale raději se jim vyhýbejte, pro lepší bezpečnost.",
"api_key_expiration": "Vypršení platnosti API klíče",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Pošlete uživateli e-mail, jakmile jejich API klíč brzy vyprší."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Pošlete uživateli e-mail, jakmile jejich API klíč brzy vyprší.",
"authorize_device": "Autorizovat zařízení",
"the_device_has_been_authorized": "Zařízení bylo autorizováno.",
"enter_code_displayed_in_previous_step": "Zadejte kód, který byl zobrazen v předchozím kroku.",
"authorize": "Autorizovat"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs",
"logout_callback_urls": "Abmelde Callback URLs",
"public_client": "Öffentlicher Client",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Ö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",
"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.",
"name_logo": "{name} Logo",
@@ -327,20 +327,24 @@
"client_authorization": "Client-Autorisierung",
"new_client_authorization": "Neue Client-Autorisierung",
"disable_animations": "Animationen deaktivieren",
"turn_off_all_animations_throughout_the_admin_ui": "Schalte alle Animationen im Admin UI aus.",
"turn_off_all_animations_throughout_the_admin_ui": "Deaktiviert alle Animationen in der Benutzeroberfläche.",
"user_disabled": "Account deaktiviert",
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
"user_disabled_successfully": "User has been disabled successfully.",
"user_enabled_successfully": "User has been enabled successfully.",
"disabled_users_cannot_log_in_or_use_services": "Deaktivierte Benutzer können sich nicht anmelden oder Dienste nutzen.",
"user_disabled_successfully": "Der Benutzer wurde erfolgreich deaktiviert.",
"user_enabled_successfully": "Der Benutzer wurde erfolgreich aktiviert.",
"status": "Status",
"disable_firstname_lastname": "Disable {firstName} {lastName}",
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
"login_code_email_success": "The login code has been sent to the user.",
"send_email": "Send Email",
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
"disable_firstname_lastname": "Deaktiviere {firstName} {lastName}",
"are_you_sure_you_want_to_disable_this_user": "Bist du sicher, dass du diesen Benutzer deaktivieren möchtest? Er kann sich dann nicht mehr anmelden, oder auf Dienste zugreifen.",
"ldap_soft_delete_users": "Deaktivierte Benutzer von LDAP behalten.",
"ldap_soft_delete_users_description": "Wenn aktiviert, werden vom LDAP gelöschte Benutzer deaktivert und nicht aus dem System gelöscht.",
"login_code_email_success": "Der Login-Code wurde an den Benutzer gesendet.",
"send_email": "E-Mail senden",
"show_code": "Code anzeigen",
"callback_url_description": "URL(s) die von deinem Client bereitgestellt werden. Wildcards (*) werden unterstützt, sollten für bessere Sicherheit jedoch lieber vermieden werden.",
"api_key_expiration": "API Key Ablauf",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Sende eine E-Mail an den Benutzer, wenn sein API Key ablaufen wird.",
"authorize_device": "Gerät autorisieren",
"the_device_has_been_authorized": "Das Gerät wurde autorisiert.",
"enter_code_displayed_in_previous_step": "Gib den Code ein, der im vorherigen Schritt angezeigt wurde.",
"authorize": "Autorisieren"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
"name_logo": "{name} logo",

View File

@@ -1,108 +1,108 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "My Account",
"logout": "Logout",
"confirm": "Confirm",
"key": "Key",
"value": "Value",
"remove_custom_claim": "Remove custom claim",
"add_custom_claim": "Add custom claim",
"add_another": "Add another",
"select_a_date": "Select a date",
"select_file": "Select File",
"profile_picture": "Profile Picture",
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.",
"click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.",
"image_should_be_in_format": "The image should be in PNG or JPEG format.",
"items_per_page": "Items per page",
"no_items_found": "No items found",
"search": "Search...",
"expand_card": "Expand card",
"copied": "Copied",
"click_to_copy": "Click to copy",
"something_went_wrong": "Something went wrong",
"go_back_to_home": "Go back to home",
"dont_have_access_to_your_passkey": "Don't have access to your passkey?",
"login_background": "Login background",
"my_account": "Mi Cuenta",
"logout": "Cerrar sesión",
"confirm": "Confirmar",
"key": "Clave",
"value": "Valor",
"remove_custom_claim": "Eliminar reclamo personalizado",
"add_custom_claim": "Añadir reclamo personalizado",
"add_another": "Añadir otro",
"select_a_date": "Seleccione una fecha",
"select_file": "Seleccione Archivo:",
"profile_picture": "Foto de perfil",
"profile_picture_is_managed_by_ldap_server": "La imagen de perfil es administrada por el servidor LDAP y no puede ser cambiada aquí.",
"click_profile_picture_to_upload_custom": "Haga clic en la imagen de perfil para subir una personalizada desde sus archivos.",
"image_should_be_in_format": "La imagen debe ser en formato PNG o JPEG.",
"items_per_page": "Elementos por página",
"no_items_found": "No se encontraron elementos",
"search": "Buscar...",
"expand_card": "Ampliar tarjeta",
"copied": "Copiado",
"click_to_copy": "Haz clic para copiar",
"something_went_wrong": "Algo ha salido mal",
"go_back_to_home": "Volver al Inicio",
"dont_have_access_to_your_passkey": "¿No tiene acceso a su Passkey?",
"login_background": "Fondo de página de acceso",
"logo": "Logo",
"login_code": "Login Code",
"create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.",
"one_hour": "1 hour",
"twelve_hours": "12 hours",
"one_day": "1 day",
"one_week": "1 week",
"one_month": "1 month",
"expiration": "Expiration",
"generate_code": "Generate Code",
"name": "Name",
"browser_unsupported": "Browser unsupported",
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please use an alternative sign in method.",
"an_unknown_error_occurred": "An unknown error occurred",
"authentication_process_was_aborted": "The authentication process was aborted",
"error_occurred_with_authenticator": "An error occurred with the authenticator",
"authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials",
"authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys",
"passkey_was_previously_registered": "This passkey was previously registered",
"authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms",
"authenticator_timed_out": "The authenticator timed out",
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.",
"sign_in_to": "Sign in to {name}",
"client_not_found": "Client not found",
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to <b>{client}</b> with your <b>{appName}</b> account?",
"email": "Email",
"view_your_email_address": "View your email address",
"profile": "Profile",
"view_your_profile_information": "View your profile information",
"groups": "Groups",
"view_the_groups_you_are_a_member_of": "View the groups you are a member of",
"cancel": "Cancel",
"sign_in": "Sign in",
"try_again": "Try again",
"client_logo": "Client Logo",
"sign_out": "Sign out",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?",
"sign_in_to_appname": "Sign in to {appName}",
"please_try_to_sign_in_again": "Please try to sign in again.",
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.",
"authenticate": "Authenticate",
"appname_setup": "{appName} Setup",
"please_try_again": "Please try again.",
"you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.",
"continue": "Continue",
"alternative_sign_in": "Alternative Sign In",
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you don't have access to your passkey, you can sign in using one of the following methods.",
"use_your_passkey_instead": "Use your passkey instead?",
"email_login": "Email Login",
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
"request_a_login_code_via_email": "Request a login code via email.",
"go_back": "Go back",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.",
"enter_code": "Enter code",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.",
"your_email": "Your email",
"submit": "Submit",
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.",
"code": "Code",
"invalid_redirect_url": "Invalid redirect URL",
"audit_log": "Audit Log",
"users": "Users",
"user_groups": "User Groups",
"oidc_clients": "OIDC Clients",
"api_keys": "API Keys",
"application_configuration": "Application Configuration",
"settings": "Settings",
"update_pocket_id": "Update Pocket ID",
"powered_by": "Powered by",
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
"time": "Time",
"event": "Event",
"approximate_location": "Approximate Location",
"ip_address": "IP Address",
"device": "Device",
"client": "Client",
"unknown": "Unknown",
"account_details_updated_successfully": "Account details updated successfully",
"login_code": "Código de inicio de sesión",
"create_a_login_code_to_sign_in_without_a_passkey_once": "Crear un código de acceso que el usuario pueda utilizar para iniciar sesión sin un Passkey una vez.",
"one_hour": "1 hora",
"twelve_hours": "12 horas",
"one_day": "1 día",
"one_week": "1 semana",
"one_month": "1 mes",
"expiration": "Expiración",
"generate_code": "Gerar Código",
"name": "Nombre",
"browser_unsupported": "Navegador no soportado",
"this_browser_does_not_support_passkeys": "Este navegador no soporta Passkeys. Por favor, utilice un método de inicio de sesión alternativo.",
"an_unknown_error_occurred": "Ocurrió un error desconocido",
"authentication_process_was_aborted": "El proceso de autenticación fue abortado",
"error_occurred_with_authenticator": "Ha ocurrido un error con el autenticador",
"authenticator_does_not_support_discoverable_credentials": "El autenticador no soporta credenciales detectables",
"authenticator_does_not_support_resident_keys": "El autenticador no soporta claves residentes",
"passkey_was_previously_registered": "Esta Passkey ha sido registrado previamente",
"authenticator_does_not_support_any_of_the_requested_algorithms": "El autenticador no soporta ninguno de los algoritmos solicitados",
"authenticator_timed_out": "Se agotó el tiempo de espera del autenticador",
"critical_error_occurred_contact_administrator": "Ha ocurrido un error crítico. Por favor, contacte a su administrador.",
"sign_in_to": "Iniciar sesión en {name}",
"client_not_found": "Cliente no encontrado",
"client_wants_to_access_the_following_information": "<b>{client}</b> quiere acceder a la siguiente información:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "¿Quieres iniciar sesión en <b>{client}</b> con tu cuenta <b>{appName}</b>?",
"email": "Correo electrónico",
"view_your_email_address": "Ver su dirección de correo electrónico",
"profile": "Perfil",
"view_your_profile_information": "Ver información de su perfil",
"groups": "Grupos",
"view_the_groups_you_are_a_member_of": "Ver los grupos de los que usted es miembro",
"cancel": "Cancelar",
"sign_in": "Iniciar sesión",
"try_again": "Intentar de nuevo",
"client_logo": "Logo del cliente",
"sign_out": "Cerrar sesión",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "¿Quieres cerrar sesión de Pocket ID con la cuenta <b>{username}</b>?",
"sign_in_to_appname": "Iniciar sesión en {appName}",
"please_try_to_sign_in_again": "Por favor, intente iniciar sesión de nuevo.",
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Autenticar con tu Passkey para acceder al panel de administración.",
"authenticate": "Autenticar",
"appname_setup": "Configuración de {appName}",
"please_try_again": "Por favor intente nuevamente.",
"you_are_about_to_sign_in_to_the_initial_admin_account": "Estás a punto de iniciar sesión en la cuenta de administrador inicial. Cualquiera con este enlace puede acceder a la cuenta hasta que se agregue un Passkey. Por favor, configure un Passkey lo antes posible para evitar acceso no autorizado.",
"continue": "Continuar",
"alternative_sign_in": "Inicio de sesión alternativa",
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Si no tiene acceso a su Passkey, puede iniciar sesión usando uno de los siguientes métodos.",
"use_your_passkey_instead": "¿Utilizar su Passkey en su lugar?",
"email_login": "Ingreso con Email",
"enter_a_login_code_to_sign_in": "Introduzca un código de acceso para iniciar sesión.",
"request_a_login_code_via_email": "Solicitar un código de acceso por correo electrónico.",
"go_back": "Volver atrás",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Se ha enviado un correo electrónico al correo proporcionado, si existe en el sistema.",
"enter_code": "Ingresa el código",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Introduzca su dirección de correo electrónico para recibir un correo electrónico con un código de acceso.",
"your_email": "Su correo electrónico",
"submit": "Enviar",
"enter_the_code_you_received_to_sign_in": "Ingrese el código que recibió para iniciar sesión.",
"code": "Código",
"invalid_redirect_url": "URL de redirección no válido",
"audit_log": "Registro de Auditoría",
"users": "Usuarios",
"user_groups": "Grupos de usuario",
"oidc_clients": "Clientes OIDC",
"api_keys": "Llaves API",
"application_configuration": "Configuración de la aplicación",
"settings": "Configuración",
"update_pocket_id": "Actualizar Pocket ID",
"powered_by": "Producido por Pocket ID",
"see_your_account_activities_from_the_last_3_months": "Vea las actividad de su cuenta de los últimos 3 meses.",
"time": "Tiempo",
"event": "Evento",
"approximate_location": "Ubicación aproximada",
"ip_address": "Dirección IP",
"device": "Dispositivo",
"client": "Cliente",
"unknown": "Desconocido",
"account_details_updated_successfully": "Detalles de la cuenta actualizados exitosamente",
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
"account_settings": "Account Settings",
"passkey_missing": "Passkey missing",
@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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",
"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.",
"name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
"authorize_device": "Authorize Device",
"the_device_has_been_authorized": "The device has been authorized.",
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "URL de callback",
"logout_callback_urls": "URL de callback de déconnexion",
"public_client": "Client public",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et linterception de code dautorisation.",
"name_logo": "Logo {name}",
@@ -342,5 +342,9 @@
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
"authorize_device": "Authorize Device",
"the_device_has_been_authorized": "The device has been authorized.",
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize"
}

View File

@@ -93,7 +93,7 @@
"application_configuration": "Configurazione dell'applicazione",
"settings": "Impostazioni",
"update_pocket_id": "Aggiorna Pocket ID",
"powered_by": "Alimentato da",
"powered_by": "Powered by",
"see_your_account_activities_from_the_last_3_months": "Visualizza le attività del tuo account degli ultimi 3 mesi.",
"time": "Ora",
"event": "Evento",
@@ -276,7 +276,7 @@
"callback_urls": "URL di callback",
"logout_callback_urls": "URL di callback per il logout",
"public_client": "Client pubblico",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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",
"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.",
"name_logo": "Logo di {name}",
@@ -342,5 +342,9 @@
"show_code": "Mostra codice",
"callback_url_description": "URL forniti dal tuo client. Wildcard (*) sono supportati, ma meglio evitarli per una migliore sicurezza.",
"api_key_expiration": "Scadenza Chiave API",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Invia un'email all'utente quando la sua chiave API sta per scadere."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Invia un'email all'utente quando la sua chiave API sta per scadere.",
"authorize_device": "Autorizza Dispositivo",
"the_device_has_been_authorized": "Il dispositivo è stato autorizzato.",
"enter_code_displayed_in_previous_step": "Inserisci il codice visualizzato nel passaggio precedente.",
"authorize": "Autorizza"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback-URL's",
"logout_callback_urls": "Callback-URL's voor afmelden",
"public_client": "Publieke client",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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 uw client een SPA of mobiele app is.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.",
"name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
"authorize_device": "Authorize Device",
"the_device_has_been_authorized": "The device has been authorized.",
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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",
"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.",
"name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
"authorize_device": "Authorize Device",
"the_device_has_been_authorized": "The device has been authorized.",
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "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",
"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.",
"name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
"authorize_device": "Authorize Device",
"the_device_has_been_authorized": "The device has been authorized.",
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize"
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs",
"public_client": "Публичный клиент",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
"public_clients_description": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
"name_logo": "Логотип {name}",
@@ -342,5 +342,9 @@
"show_code": "Показать код",
"callback_url_description": "URL-адреса, предоставленные клиентом. Поддерживаются wildcard-адреса (*), но лучше всего избегать их для лучшей безопасности.",
"api_key_expiration": "Истечение срока действия API ключа",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда истечет срок действия API ключа."
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда истечет срок действия API ключа.",
"authorize_device": "Авторизовать устройство",
"the_device_has_been_authorized": "Устройство авторизовано.",
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
"authorize": "Авторизируйте"
}

View File

@@ -1,346 +1,346 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "账户",
"logout": "登出",
"confirm": "确认",
"key": "Key",
"value": "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": "返回首页",
"dont_have_access_to_your_passkey": "无法使用您的通行密钥?试试其他登录方式。",
"login_background": "登录页背景图",
"logo": "Logo",
"login_code": "临时登录码",
"create_a_login_code_to_sign_in_without_a_passkey_once": "创建一个临时登录码,用户可以使用它一次性登录而无需通行密钥。",
"one_hour": "1 小时",
"twelve_hours": "12 小时",
"one_day": "1 天",
"one_week": "1 周",
"one_month": "1 个月",
"expiration": "到期时间",
"generate_code": "生成代码",
"name": "名称",
"browser_unsupported": "浏览器不支持",
"this_browser_does_not_support_passkeys": "此浏览器不支持通行密钥。请使用其他登录方式。",
"an_unknown_error_occurred": "发生未知错误",
"authentication_process_was_aborted": "认证过程被中止",
"error_occurred_with_authenticator": "认证器发生错误",
"authenticator_does_not_support_discoverable_credentials": "认证器不支持可发现的凭据",
"authenticator_does_not_support_resident_keys": "认证器不支持常驻密钥",
"passkey_was_previously_registered": "此通行密钥之前已注册",
"authenticator_does_not_support_any_of_the_requested_algorithms": "认证器不支持任何请求的算法",
"authenticator_timed_out": "认证器超时",
"critical_error_occurred_contact_administrator": "发生严重错误。请联系您的管理员。",
"sign_in_to": "登录到 {name}",
"client_not_found": "客户端未找到",
"client_wants_to_access_the_following_information": "<b>{client}</b> 希望访问以下信息:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "您是否希望使用您的 <b>{appName}</b> 账户登录到 <b>{client}</b>",
"email": "电子邮件",
"view_your_email_address": "查看您的电子邮件地址",
"profile": "个人资料",
"view_your_profile_information": "查看您的个人资料信息",
"groups": "群组",
"view_the_groups_you_are_a_member_of": "查看您所属的群组",
"cancel": "取消",
"sign_in": "登录",
"try_again": "重试",
"client_logo": "客户端标志",
"sign_out": "登出",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "您是否希望使用账户 <b>{username}</b> 登出 Pocket ID",
"sign_in_to_appname": "登录到 {appName}",
"please_try_to_sign_in_again": "请尝试重新登录。",
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "使用通行密钥或通过临时登录码进行登录",
"authenticate": "登录",
"appname_setup": "{appName} 设置",
"please_try_again": "请重试。",
"you_are_about_to_sign_in_to_the_initial_admin_account": "您即将登录到初始管理员账户。在此添加通行密钥之前,任何拥有此链接的人都可以访问该账户。请尽快设置通行密钥以防止未经授权的访问。",
"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": "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": "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": "可选描述,帮助识别此密钥的用途。",
"name_must_be_at_least_3_characters": "名称必须至少为 3 个字符",
"name_cannot_exceed_50_characters": "名称不能超过 50 个字符",
"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 Bind DN",
"ldap_bind_password": "LDAP Bind Password",
"ldap_base_dn": "LDAP Base DN",
"user_search_filter": "User Search Filter",
"the_search_filter_to_use_to_search_or_sync_users": "用于搜索/同步用户的搜索过滤器。",
"groups_search_filter": "Groups Search Filter",
"the_search_filter_to_use_to_search_or_sync_groups": "用于搜索/同步群组的搜索过滤器。",
"attribute_mapping": "属性映射",
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
"the_value_of_this_attribute_should_never_change": "此属性的值不应更改。",
"username_attribute": "Username Attribute",
"user_mail_attribute": "User Mail Attribute",
"user_first_name_attribute": "User First Name Attribute",
"user_last_name_attribute": "User Last Name Attribute",
"user_profile_picture_attribute": "User Profile Picture Attribute",
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "此属性的值可以是 URL、二进制或 base64 编码的图像。",
"group_members_attribute": "Group Members Attribute",
"the_attribute_to_use_for_querying_members_of_a_group": "用于查询群组成员的属性。",
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
"group_name_attribute": "Group Name Attribute",
"admin_group_name": "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 Token 中。",
"user_group_created_successfully": "用户组创建成功",
"create_user_group": "创建用户组",
"create_a_new_group_that_can_be_assigned_to_users": "创建一个可以分配给用户的新群组。",
"add_group": "添加群组",
"manage_user_groups": "管理用户组",
"friendly_name": "显示名称",
"name_that_will_be_displayed_in_the_ui": "将在用户界面中显示的名称",
"name_that_will_be_in_the_groups_claim": "将在 \"groups\" 声明中显示的名称",
"delete_name": "删除 {name}",
"are_you_sure_you_want_to_delete_this_user_group": "您确定要删除此用户组吗?",
"user_group_deleted_successfully": "用户组删除成功",
"user_count": "用户数",
"user_group_updated_successfully": "用户组更新成功",
"users_updated_successfully": "用户更新成功",
"user_group_details_name": "用户组详情 {name}",
"assign_users_to_this_group": "将用户分配到此群组。",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "自定义声明是键值对,可用于存储有关用户的额外信息。如果请求了 'profile' 范围,这些声明将包含在 ID 令牌中。如果存在冲突,用户上定义的自定义声明将优先。",
"oidc_client_created_successfully": "OIDC 客户端创建成功",
"create_oidc_client": "创建 OIDC 客户端",
"add_a_new_oidc_client_to_appname": "向 {appName} 添加新的 OIDC 客户端。",
"add_oidc_client": "添加 OIDC 客户端",
"manage_oidc_clients": "管理 OIDC 客户端",
"one_time_link": "一次性链接",
"use_this_link_to_sign_in_once": "使用此链接一次性登录。这对于尚未添加通行密钥或丢失通行密钥的用户是必要的。",
"add": "添加",
"callback_urls": "Callback URL",
"logout_callback_urls": "Logout Callback URL",
"public_client": "公共客户端",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "公共客户端没有客户端密钥,而是使用 PKCE。如果您的客户端是 SPA 或移动应用,请启用此选项。",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
"name_logo": "{name} Logo",
"change_logo": "更改 Logo",
"upload_logo": "上传 Logo",
"remove_logo": "移除 Logo",
"are_you_sure_you_want_to_delete_this_oidc_client": "您确定要删除此 OIDC 客户端吗?",
"oidc_client_deleted_successfully": "OIDC 客户端删除成功",
"authorization_url": "Authorization URL",
"oidc_discovery_url": "OIDC Discovery URL",
"token_url": "Token URL",
"userinfo_url": "Userinfo URL",
"logout_url": "Logout URL",
"certificate_url": "Certificate URL",
"enabled": "已启用",
"disabled": "已禁用",
"oidc_client_updated_successfully": "OIDC 客户端更新成功",
"create_new_client_secret": "创建新的客户端密钥",
"are_you_sure_you_want_to_create_a_new_client_secret": "您确定要创建新的客户端密钥吗?旧的密钥将被失效。",
"generate": "生成",
"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": "浅色模式 Logo",
"dark_mode_logo": "深色模式 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": "选择您要使用的语言。某些语言可能未完全翻译。",
"personal": "个人",
"global": "全局",
"all_users": "所有用户",
"all_events": "所有事件",
"all_clients": "所有客户端",
"global_audit_log": "全局日志",
"see_all_account_activities_from_the_last_3_months": "查看过去 3 个月的所有用户活动。",
"token_sign_in": "Token 登录",
"client_authorization": "客户端授权",
"new_client_authorization": "首次客户端授权",
"disable_animations": "禁用动画",
"turn_off_all_animations_throughout_the_admin_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。支持通配符 (*),但为了更好的安全性最好避免使用。",
"api_key_expiration": "API 密钥过期",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "当用户的 API 密钥即将过期时,向其发送电子邮件。"
"$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "账户",
"logout": "登出",
"confirm": "确认",
"key": "Key",
"value": "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": "返回首页",
"dont_have_access_to_your_passkey": "无法使用您的通行密钥?试试其他登录方式。",
"login_background": "登录页背景图",
"logo": "Logo",
"login_code": "临时登录码",
"create_a_login_code_to_sign_in_without_a_passkey_once": "创建一个临时登录码,用户可以使用它一次性登录而无需通行密钥。",
"one_hour": "1 小时",
"twelve_hours": "12 小时",
"one_day": "1 天",
"one_week": "1 周",
"one_month": "1 个月",
"expiration": "到期时间",
"generate_code": "生成代码",
"name": "名称",
"browser_unsupported": "浏览器不支持",
"this_browser_does_not_support_passkeys": "此浏览器不支持通行密钥。请使用其他登录方式。",
"an_unknown_error_occurred": "发生未知错误",
"authentication_process_was_aborted": "认证过程被中止",
"error_occurred_with_authenticator": "认证器发生错误",
"authenticator_does_not_support_discoverable_credentials": "认证器不支持可发现的凭据",
"authenticator_does_not_support_resident_keys": "认证器不支持常驻密钥",
"passkey_was_previously_registered": "此通行密钥之前已注册",
"authenticator_does_not_support_any_of_the_requested_algorithms": "认证器不支持任何请求的算法",
"authenticator_timed_out": "认证器超时",
"critical_error_occurred_contact_administrator": "发生严重错误。请联系您的管理员。",
"sign_in_to": "登录到 {name}",
"client_not_found": "客户端未找到",
"client_wants_to_access_the_following_information": "<b>{client}</b> 希望访问以下信息:",
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "您是否希望使用您的 <b>{appName}</b> 账户登录到 <b>{client}</b>",
"email": "电子邮件",
"view_your_email_address": "查看您的电子邮件地址",
"profile": "个人资料",
"view_your_profile_information": "查看您的个人资料信息",
"groups": "群组",
"view_the_groups_you_are_a_member_of": "查看您所属的群组",
"cancel": "取消",
"sign_in": "登录",
"try_again": "重试",
"client_logo": "客户端标志",
"sign_out": "登出",
"do_you_want_to_sign_out_of_pocketid_with_the_account": "您是否希望使用账户 <b>{username}</b> 登出 Pocket ID",
"sign_in_to_appname": "登录到 {appName}",
"please_try_to_sign_in_again": "请尝试重新登录。",
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "使用通行密钥或通过临时登录码进行登录",
"authenticate": "登录",
"appname_setup": "{appName} 设置",
"please_try_again": "请重试。",
"you_are_about_to_sign_in_to_the_initial_admin_account": "您即将登录到初始管理员账户。在此添加通行密钥之前,任何拥有此链接的人都可以访问该账户。请尽快设置通行密钥以防止未经授权的访问。",
"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": "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": "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": "可选描述,帮助识别此密钥的用途。",
"name_must_be_at_least_3_characters": "名称必须至少为 3 个字符",
"name_cannot_exceed_50_characters": "名称不能超过 50 个字符",
"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 Bind DN",
"ldap_bind_password": "LDAP Bind Password",
"ldap_base_dn": "LDAP Base DN",
"user_search_filter": "User Search Filter",
"the_search_filter_to_use_to_search_or_sync_users": "用于搜索/同步用户的搜索过滤器。",
"groups_search_filter": "Groups Search Filter",
"the_search_filter_to_use_to_search_or_sync_groups": "用于搜索/同步群组的搜索过滤器。",
"attribute_mapping": "属性映射",
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
"the_value_of_this_attribute_should_never_change": "此属性的值不应更改。",
"username_attribute": "Username Attribute",
"user_mail_attribute": "User Mail Attribute",
"user_first_name_attribute": "User First Name Attribute",
"user_last_name_attribute": "User Last Name Attribute",
"user_profile_picture_attribute": "User Profile Picture Attribute",
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "此属性的值可以是 URL、二进制或 base64 编码的图像。",
"group_members_attribute": "Group Members Attribute",
"the_attribute_to_use_for_querying_members_of_a_group": "用于查询群组成员的属性。",
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
"group_name_attribute": "Group Name Attribute",
"admin_group_name": "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 Token 中。",
"user_group_created_successfully": "用户组创建成功",
"create_user_group": "创建用户组",
"create_a_new_group_that_can_be_assigned_to_users": "创建一个可以分配给用户的新群组。",
"add_group": "添加群组",
"manage_user_groups": "管理用户组",
"friendly_name": "显示名称",
"name_that_will_be_displayed_in_the_ui": "将在用户界面中显示的名称",
"name_that_will_be_in_the_groups_claim": "将在 \"groups\" 声明中显示的名称",
"delete_name": "删除 {name}",
"are_you_sure_you_want_to_delete_this_user_group": "您确定要删除此用户组吗?",
"user_group_deleted_successfully": "用户组删除成功",
"user_count": "用户数",
"user_group_updated_successfully": "用户组更新成功",
"users_updated_successfully": "用户更新成功",
"user_group_details_name": "用户组详情 {name}",
"assign_users_to_this_group": "将用户分配到此群组。",
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "自定义声明是键值对,可用于存储有关用户的额外信息。如果请求了 'profile' 范围,这些声明将包含在 ID 令牌中。如果存在冲突,用户上定义的自定义声明将优先。",
"oidc_client_created_successfully": "OIDC 客户端创建成功",
"create_oidc_client": "创建 OIDC 客户端",
"add_a_new_oidc_client_to_appname": "向 {appName} 添加新的 OIDC 客户端。",
"add_oidc_client": "添加 OIDC 客户端",
"manage_oidc_clients": "管理 OIDC 客户端",
"one_time_link": "一次性链接",
"use_this_link_to_sign_in_once": "使用此链接一次性登录。这对于尚未添加通行密钥或丢失通行密钥的用户是必要的。",
"add": "添加",
"callback_urls": "Callback URL",
"logout_callback_urls": "Logout Callback URL",
"public_client": "公共客户端",
"public_clients_description": "公共客户端没有客户端密钥,而是使用 PKCE。如果您的客户端是 SPA 或移动应用,请启用此选项。",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
"name_logo": "{name} Logo",
"change_logo": "更改 Logo",
"upload_logo": "上传 Logo",
"remove_logo": "移除 Logo",
"are_you_sure_you_want_to_delete_this_oidc_client": "您确定要删除此 OIDC 客户端吗?",
"oidc_client_deleted_successfully": "OIDC 客户端删除成功",
"authorization_url": "Authorization URL",
"oidc_discovery_url": "OIDC Discovery URL",
"token_url": "Token URL",
"userinfo_url": "Userinfo URL",
"logout_url": "Logout URL",
"certificate_url": "Certificate URL",
"enabled": "已启用",
"disabled": "已禁用",
"oidc_client_updated_successfully": "OIDC 客户端更新成功",
"create_new_client_secret": "创建新的客户端密钥",
"are_you_sure_you_want_to_create_a_new_client_secret": "您确定要创建新的客户端密钥吗?旧的密钥将被失效。",
"generate": "生成",
"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": "浅色模式 Logo",
"dark_mode_logo": "深色模式 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": "选择您要使用的语言。某些语言可能未完全翻译。",
"personal": "个人",
"global": "全局",
"all_users": "所有用户",
"all_events": "所有事件",
"all_clients": "所有客户端",
"global_audit_log": "全局日志",
"see_all_account_activities_from_the_last_3_months": "查看过去 3 个月的所有用户活动。",
"token_sign_in": "Token 登录",
"client_authorization": "客户端授权",
"new_client_authorization": "首次客户端授权",
"disable_animations": "禁用动画",
"turn_off_all_animations_throughout_the_admin_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。支持通配符 (*),但为了更好的安全性最好避免使用。",
"api_key_expiration": "API 密钥过期",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "当用户的 API 密钥即将过期时,向其发送电子邮件。"
}

10989
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "0.50.0",
"version": "0.53.0",
"private": true,
"type": "module",
"scripts": {
@@ -57,6 +57,6 @@
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0",
"vite": "^6.2.6"
"vite": "^6.3.4"
}
}

View File

@@ -23,33 +23,34 @@ const paraglideHandle: Handle = ({ event, resolve }) => {
const authenticationHandle: Handle = async ({ event, resolve }) => {
const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const isUnauthenticatedOnlyPath = event.url.pathname.startsWith('/login') || event.url.pathname.startsWith('/lc')
const isPublicPath = ['/authorize', '/device', '/health'].includes(event.url.pathname);
const isAdminPath = event.url.pathname.startsWith('/settings/admin');
const path = event.url.pathname;
const isUnauthenticatedOnlyPath =
path == '/login' || path.startsWith('/login/') || path == '/lc' || path.startsWith('/lc/');
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
if (!isUnauthenticatedOnlyPath && !isPublicPath && !isSignedIn) {
return new Response(null, {
status: 302,
status: 303,
headers: { location: '/login' }
});
}
if (isUnauthenticatedOnlyPath && isSignedIn) {
return new Response(null, {
status: 302,
status: 303,
headers: { location: '/settings' }
});
}
if (isAdminPath && !isAdmin) {
return new Response(null, {
status: 302,
status: 303,
headers: { location: '/settings' }
});
}
const response = await resolve(event);
return response;
return resolve(event);
};
export const handle: Handle = sequence(paraglideHandle, authenticationHandle);
@@ -79,7 +80,7 @@ function verifyJwt(accessToken: string | undefined) {
const jwtPayload = decodeJwt<{ isAdmin: boolean }>(accessToken);
if (jwtPayload?.exp && jwtPayload.exp * 1000 > Date.now()) {
isSignedIn = true;
isAdmin = !!(jwtPayload?.isAdmin);
isAdmin = !!jwtPayload?.isAdmin;
}
}

View File

@@ -37,7 +37,7 @@
: (auditLogs = await auditLogService.list(options))}
columns={[
{ label: m.time(), sortColumn: 'createdAt' },
...(isAdmin ? [{ label: 'Username' }] : []),
...(isAdmin ? [{ label: 'Username' }] : []),
{ label: m.event(), sortColumn: 'event' },
{ label: m.approximate_location(), sortColumn: 'city' },
{ label: m.ip_address(), sortColumn: 'ipAddress' },

View File

@@ -29,7 +29,9 @@
</script>
<Tooltip.Root closeOnPointerDown={false} {onOpenChange} {open}>
<Tooltip.Trigger class="text-start" tabindex={-1} onclick={onClick}>{@render children()}</Tooltip.Trigger>
<Tooltip.Trigger class="text-start" tabindex={-1} onclick={onClick}
>{@render children()}</Tooltip.Trigger
>
<Tooltip.Content onclick={copyToClipboard}>
{#if copied}
<span class="flex items-center"><LucideCheck class="mr-1 h-4 w-4" /> {m.copied()}</span>

View File

@@ -7,7 +7,7 @@
</script>
<div class="mt-[20%] flex flex-col items-center">
<LucideXCircle class="h-12 w-12 text-muted-foreground" />
<LucideXCircle class="text-muted-foreground h-12 w-12" />
<h1 class="mt-3 text-2xl font-semibold">{m.something_went_wrong()}</h1>
<p class="text-muted-foreground">{message}</p>
{#if showButton}

View File

@@ -92,7 +92,7 @@
onkeydown={(e) => {
if (e.key === 'Enter') handleSuggestionClick(suggestion);
}}
class="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 {selectedIndex ===
class="hover:bg-accent hover:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 {selectedIndex ===
index
? 'bg-accent text-accent-foreground'
: ''}"

View File

@@ -27,7 +27,7 @@
bind:checked
/>
<div class="grid gap-1.5 leading-none">
<Label for={id} class="mb-0 text-sm font-medium leading-none">
<Label for={id} class="mb-0 text-sm leading-none font-medium">
{label}
</Label>
{#if description}

View File

@@ -17,7 +17,7 @@
onInput,
...restProps
}: HTMLAttributes<HTMLDivElement> & {
input?: FormInput<string | boolean | number | Date>;
input?: FormInput<string | boolean | number | Date | undefined>;
label?: string;
description?: string;
placeholder?: string;

View File

@@ -24,7 +24,7 @@
<DropdownMenu.Content class="min-w-40" align="start">
<DropdownMenu.Label class="font-normal">
<div class="flex flex-col space-y-1">
<p class="text-sm font-medium leading-none">
<p class="text-sm leading-none font-medium">
{$userStore?.firstName}
{$userStore?.lastName}
</p>

View File

@@ -50,7 +50,7 @@
</div>
<!-- Background image with slide animation -->
<div class="{cn(animate && 'animate-slide-bg-container')} absolute bottom-0 right-0 top-0 z-0">
<div class="{cn(animate && 'animate-slide-bg-container')} absolute top-0 right-0 bottom-0 z-0">
<img
src="/api/application-configuration/background-image"
class="h-screen rounded-l-[60px] object-cover {animate ? 'w-full' : 'w-[calc(100vw-650px)]'}"

View File

@@ -116,7 +116,7 @@
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
<Separator />
<p class="text-nowrap text-xs">{m.or_visit()}</p>
<p class="text-xs text-nowrap">{m.or_visit()}</p>
<Separator />
</div>

View File

@@ -5,9 +5,9 @@
</script>
<div class="flex items-center">
<div class="mr-5 rounded-lg bg-muted p-2"><svelte:component this={icon} /></div>
<div class="bg-muted mr-5 rounded-lg p-2"><svelte:component this={icon} /></div>
<div class="text-start">
<h3 class="font-semibold">{name}</h3>
<p class="text-sm text-muted-foreground">{description}</p>
<p class="text-muted-foreground text-sm">{description}</p>
</div>
</div>

View File

@@ -18,7 +18,7 @@
{transition}
{transitionConfig}
class={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
'bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full',
className
)}
{...$$restProps}

View File

@@ -9,7 +9,7 @@
</script>
<AlertDialogPrimitive.Description
class={cn('text-sm text-muted-foreground', className)}
class={cn('text-muted-foreground text-sm', className)}
{...$$restProps}
>
<slot />

View File

@@ -16,6 +16,6 @@
<AlertDialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn('fixed inset-0 z-50 bg-background/80 backdrop-blur-sm ', className)}
class={cn('bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ', className)}
{...$$restProps}
/>

View File

@@ -14,7 +14,7 @@
<svelte:element
this={level}
class={cn('mb-1 font-medium leading-none tracking-tight', className)}
class={cn('mb-1 leading-none font-medium tracking-tight', className)}
{...$$restProps}
>
<slot />

View File

@@ -37,7 +37,9 @@
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
<slot />
{#if dismissibleId}
<button on:click={dismiss} class="absolute top-0 right-0 m-3 text-black dark:text-white"><LucideX class="w-4" /></button>
<button on:click={dismiss} class="absolute top-0 right-0 m-3 text-black dark:text-white"
><LucideX class="w-4" /></button
>
{/if}
</div>
{/if}

View File

@@ -9,7 +9,7 @@
</script>
<AvatarPrimitive.Fallback
class={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
class={cn('bg-muted flex h-full w-full items-center justify-center rounded-full', className)}
{...$$restProps}
>
<slot />

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.CellProps;
export let date: $$Props["date"];
let className: $$Props["class"] = undefined;
export let date: $$Props['date'];
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Cell
{date}
class={cn(
"[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md",
'[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md',
className
)}
{...$$restProps}

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.DayProps;
type $$Events = CalendarPrimitive.DayEvents;
export let date: $$Props["date"];
export let month: $$Props["month"];
let className: $$Props["class"] = undefined;
export let date: $$Props['date'];
export let month: $$Props['month'];
let className: $$Props['class'] = undefined;
export { className as class };
</script>
@@ -17,17 +17,17 @@
{date}
{month}
class={cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal ",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground",
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal ',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground data-[selected]:opacity-100",
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground data-[selected]:opacity-100',
// Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
"data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through",
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months
"data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30",
'data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30',
className
)}
{...$$restProps}

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.GridBodyProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.GridHeadProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.GridRowProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.GridRow class={cn("flex", className)} {...$$restProps}>
<CalendarPrimitive.GridRow class={cn('flex', className)} {...$$restProps}>
<slot />
</CalendarPrimitive.GridRow>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.GridProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Grid class={cn("w-full border-collapse space-y-1", className)} {...$$restProps}>
<CalendarPrimitive.Grid class={cn('w-full border-collapse space-y-1', className)} {...$$restProps}>
<slot />
</CalendarPrimitive.Grid>

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.HeadCellProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.HeadCell
class={cn("text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal", className)}
class={cn('text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal', className)}
{...$$restProps}
>
<slot />

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.HeaderProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Header
class={cn("relative flex w-full items-center justify-between pt-1", className)}
class={cn('relative flex w-full items-center justify-between pt-1', className)}
{...$$restProps}
>
<slot />

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.HeadingProps;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Heading
let:headingValue
class={cn("text-sm font-medium", className)}
class={cn('text-sm font-medium', className)}
{...$$restProps}
>
<slot {headingValue}>

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/style.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/style.js';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
class={cn('mt-4 flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4', className)}
{...$$restProps}
>
<slot />

View File

@@ -1,21 +1,21 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.NextButtonProps;
type $$Events = CalendarPrimitive.NextButtonEvents;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.NextButton
on:click
class={cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
className
)}
{...$$restProps}

View File

@@ -1,21 +1,21 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils/style.js";
import { Calendar as CalendarPrimitive } from 'bits-ui';
import ChevronLeft from 'lucide-svelte/icons/chevron-left';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js';
type $$Props = CalendarPrimitive.PrevButtonProps;
type $$Events = CalendarPrimitive.PrevButtonEvents;
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<CalendarPrimitive.PrevButton
on:click
class={cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
className
)}
{...$$restProps}

View File

@@ -1,141 +1,137 @@
<script lang="ts">
import * as Calendar from "$lib/components/ui/calendar/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import { cn } from "$lib/utils/style";
import {
DateFormatter,
getLocalTimeZone,
today
} from "@internationalized/date";
import { Calendar as CalendarPrimitive } from "bits-ui";
import * as Calendar from '$lib/components/ui/calendar/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import { cn } from '$lib/utils/style';
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date';
import { Calendar as CalendarPrimitive } from 'bits-ui';
type $$Props = CalendarPrimitive.Props;
type $$Events = CalendarPrimitive.Events;
export let value: $$Props["value"] = undefined;
export let placeholder: $$Props["placeholder"] = today(getLocalTimeZone());
export let weekdayFormat: $$Props["weekdayFormat"] = "short";
export let value: $$Props['value'] = undefined;
export let placeholder: $$Props['placeholder'] = today(getLocalTimeZone());
export let weekdayFormat: $$Props['weekdayFormat'] = 'short';
const monthOptions = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
].map((month, i) => ({ value: i + 1, label: month }));
const monthFmt = new DateFormatter("en-US", {
month: "long"
const monthFmt = new DateFormatter('en-US', {
month: 'long'
});
const yearOptions = Array.from({ length: 100 }, (_, i) => ({
label: String(new Date().getFullYear() + i),
value: new Date().getFullYear() + i
label: String(new Date().getFullYear() + i),
value: new Date().getFullYear() + i
}));
$: defaultYear = placeholder
? {
value: placeholder.year,
label: String(placeholder.year)
}
: undefined;
? {
value: placeholder.year,
label: String(placeholder.year)
}
: undefined;
$: defaultMonth = placeholder
? {
value: placeholder.month,
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined;
let className: $$Props["class"] = undefined;
? {
value: placeholder.month,
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
</script>
<CalendarPrimitive.Root
{weekdayFormat}
{weekdayFormat}
class={cn('rounded-md border p-3', className)}
{...$$restProps}
on:keydown
let:months
let:weekdays
bind:value
bind:placeholder
bind:placeholder
>
<Calendar.Header>
<Calendar.Header>
<Calendar.Heading class="flex w-full items-center justify-between gap-2">
<Select.Root
selected={defaultMonth}
items={monthOptions}
onSelectedChange={(v) => {
if (!v || !placeholder) return;
if (v.value === placeholder?.month) return;
placeholder = placeholder.set({ month: v.value });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
<Select.Value placeholder="Select month" />
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label }}
<Select.Item {value} {label}>
{label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
selected={defaultYear}
items={yearOptions}
onSelectedChange={(v) => {
if (!v || !placeholder) return;
if (v.value === placeholder?.year) return;
placeholder = placeholder.set({ year: v.value });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
<Select.Value placeholder="Select year" />
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label }}
<Select.Item {value} {label}>
{label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Calendar.Heading class="flex w-full items-center justify-between gap-2">
<Select.Root
selected={defaultMonth}
items={monthOptions}
onSelectedChange={(v) => {
if (!v || !placeholder) return;
if (v.value === placeholder?.month) return;
placeholder = placeholder.set({ month: v.value });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
<Select.Value placeholder="Select month" />
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label }}
<Select.Item {value} {label}>
{label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
selected={defaultYear}
items={yearOptions}
onSelectedChange={(v) => {
if (!v || !placeholder) return;
if (v.value === placeholder?.year) return;
placeholder = placeholder.set({ year: v.value });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
<Select.Value placeholder="Select year" />
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label }}
<Select.Item {value} {label}>
{label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</Calendar.Heading>
</Calendar.Header>
<Calendar.Months>
<Calendar.Months>
{#each months as month}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date}
<Calendar.Cell {date}>
<Calendar.Day {date} month={month.value} />
</Calendar.Cell>
{/each}
{#each months as month}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date}
<Calendar.Cell {date}>
<Calendar.Day {date} month={month.value} />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
{/each}
</Calendar.GridBody>
</Calendar.Grid>
</Calendar.Months>
</Calendar.Months>
</CalendarPrimitive.Root>

View File

@@ -1,16 +1,16 @@
import Root from "./calendar.svelte";
import Cell from "./calendar-cell.svelte";
import Day from "./calendar-day.svelte";
import Grid from "./calendar-grid.svelte";
import Header from "./calendar-header.svelte";
import Months from "./calendar-months.svelte";
import GridRow from "./calendar-grid-row.svelte";
import Heading from "./calendar-heading.svelte";
import GridBody from "./calendar-grid-body.svelte";
import GridHead from "./calendar-grid-head.svelte";
import HeadCell from "./calendar-head-cell.svelte";
import NextButton from "./calendar-next-button.svelte";
import PrevButton from "./calendar-prev-button.svelte";
import Root from './calendar.svelte';
import Cell from './calendar-cell.svelte';
import Day from './calendar-day.svelte';
import Grid from './calendar-grid.svelte';
import Header from './calendar-header.svelte';
import Months from './calendar-months.svelte';
import GridRow from './calendar-grid-row.svelte';
import Heading from './calendar-heading.svelte';
import GridBody from './calendar-grid-body.svelte';
import GridHead from './calendar-grid-head.svelte';
import HeadCell from './calendar-head-cell.svelte';
import NextButton from './calendar-next-button.svelte';
import PrevButton from './calendar-prev-button.svelte';
export {
Day,
@@ -26,5 +26,5 @@ export {
NextButton,
PrevButton,
//
Root as Calendar,
Root as Calendar
};

View File

@@ -14,7 +14,7 @@
<svelte:element
this={tag}
class={cn('flex items-center gap-2 text-xl font-semibold leading-none tracking-tight', className)}
class={cn('flex items-center gap-2 text-xl leading-none font-semibold tracking-tight', className)}
{...$$restProps}
>
<slot />

View File

@@ -14,7 +14,7 @@
<CheckboxPrimitive.Root
class={cn(
'peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50',
'peer border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground box-content h-4 w-4 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50',
className
)}
bind:checked

View File

@@ -1,19 +1,19 @@
<script lang="ts">
import type { Dialog as DialogPrimitive } from "bits-ui";
import type { Command as CommandPrimitive } from "cmdk-sv";
import Command from "./command.svelte";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import type { Dialog as DialogPrimitive } from 'bits-ui';
import type { Command as CommandPrimitive } from 'cmdk-sv';
import Command from './command.svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
type $$Props = DialogPrimitive.Props & CommandPrimitive.CommandProps;
export let open: $$Props["open"] = false;
export let value: $$Props["value"] = undefined;
export let open: $$Props['open'] = false;
export let value: $$Props['value'] = undefined;
</script>
<Dialog.Root bind:open {...$$restProps}>
<Dialog.Content class="overflow-hidden p-0 shadow-lg">
<Command
class="[&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5"
class="[&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5"
{...$$restProps}
bind:value
>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils/style.js";
import type { ClassValue } from "svelte/elements";
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils/style.js';
import type { ClassValue } from 'svelte/elements';
type $$Props = CommandPrimitive.EmptyProps;
let className: ClassValue | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Empty class={cn("py-6 text-center text-sm", className)} {...$$restProps}>
<CommandPrimitive.Empty class={cn('py-6 text-center text-sm', className)} {...$$restProps}>
<slot />
</CommandPrimitive.Empty>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils/style.js";
import type { ClassValue } from "svelte/elements";
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils/style.js';
import type { ClassValue } from 'svelte/elements';
type $$Props = CommandPrimitive.GroupProps;
let className: ClassValue | undefined | null = undefined;
@@ -10,7 +10,7 @@
<CommandPrimitive.Group
class={cn(
"text-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium",
'text-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium',
className
)}
{...$$restProps}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils/style.js";
import type { ClassValue } from "svelte/elements";
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils/style.js';
import type { ClassValue } from 'svelte/elements';
type $$Props = CommandPrimitive.ItemProps;
@@ -14,7 +14,7 @@
<CommandPrimitive.Item
{asChild}
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...$$restProps}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils/style.js";
import type { ClassValue } from "svelte/elements";
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils/style.js';
import type { ClassValue } from 'svelte/elements';
type $$Props = CommandPrimitive.ListProps;
let className: ClassValue | undefined | null = undefined;
@@ -9,7 +9,7 @@
</script>
<CommandPrimitive.List
class={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
class={cn('max-h-[300px] overflow-x-hidden overflow-y-auto', className)}
{...$$restProps}
>
<slot />

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils/style.js";
import type { ClassValue } from "svelte/elements";
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils/style.js';
import type { ClassValue } from 'svelte/elements';
type $$Props = CommandPrimitive.SeparatorProps;
let className: ClassValue | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Separator class={cn("bg-border -mx-1 h-px", className)} {...$$restProps} />
<CommandPrimitive.Separator class={cn('bg-border -mx-1 h-px', className)} {...$$restProps} />

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { ClassValue, HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/style.js";
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/style.js';
type $$Props = HTMLAttributes<HTMLSpanElement>;
@@ -9,7 +9,7 @@
</script>
<span
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
{...$$restProps}
>
<slot />

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils/style.js";
import type { ClassValue } from "svelte/elements";
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils/style.js';
import type { ClassValue } from 'svelte/elements';
type $$Props = CommandPrimitive.CommandProps;
export let value: $$Props["value"] = undefined;
export let value: $$Props['value'] = undefined;
let className: ClassValue | undefined | null = undefined;
export { className as class };
@@ -13,7 +13,7 @@
<CommandPrimitive.Root
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className
)}
bind:value

View File

@@ -1,14 +1,14 @@
import { Command as CommandPrimitive } from "cmdk-sv";
import { Command as CommandPrimitive } from 'cmdk-sv';
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
import Root from './command.svelte';
import Dialog from './command-dialog.svelte';
import Empty from './command-empty.svelte';
import Group from './command-group.svelte';
import Item from './command-item.svelte';
import Input from './command-input.svelte';
import List from './command-list.svelte';
import Separator from './command-separator.svelte';
import Shortcut from './command-shortcut.svelte';
const Loading = CommandPrimitive.Loading;
@@ -33,5 +33,5 @@ export {
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
Loading as CommandLoading
};

Some files were not shown because too many files have changed in this diff Show More