mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-28 18:26:36 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f125cf0dad | ||
|
|
6a038fcf9a | ||
|
|
76e0192cee | ||
|
|
3ebf94dd84 | ||
|
|
7ec57437ac | ||
|
|
ed2c7b2303 | ||
|
|
e03270eb9d | ||
|
|
d683d18d91 | ||
|
|
f184120890 | ||
|
|
04d8500910 | ||
|
|
93639dddb2 | ||
|
|
a190529117 | ||
|
|
73392b5837 | ||
|
|
65616f65e5 | ||
|
|
98a99fbb0a | ||
|
|
3f3b6b88fd | ||
|
|
8f98d8c0b4 | ||
|
|
c9308472a9 | ||
|
|
6362ff9861 | ||
|
|
10d640385f | ||
|
|
47927d1574 | ||
|
|
b356cef766 | ||
|
|
9fc45930a8 | ||
|
|
028d1c858e | ||
|
|
eb3963d0fc | ||
|
|
35d913f905 | ||
|
|
32485f4c7c | ||
|
|
ceb38b0825 | ||
|
|
c0b6ede5be | ||
|
|
c20e93b55c | ||
|
|
24ca6a106d | ||
|
|
9f0aa55be6 | ||
|
|
068fcc65a6 | ||
|
|
f2dfb3da5d | ||
|
|
cbf0e3117d | ||
|
|
694f266dea | ||
|
|
29fc185376 | ||
|
|
781be37416 | ||
|
|
b1f97e05a1 | ||
|
|
2c74865173 | ||
|
|
ad8a90c839 | ||
|
|
f9839a978c | ||
|
|
b81de45166 | ||
|
|
22f4254932 | ||
|
|
507f9490fa | ||
|
|
043cce615d | ||
|
|
69e2083722 | ||
|
|
d47b20326f | ||
|
|
fc9939d1f1 | ||
|
|
2c1c67b5e4 | ||
|
|
d010be4c88 | ||
|
|
01db8c0a46 | ||
|
|
fe5917d96d | ||
|
|
4f0b434c54 | ||
|
|
6bdf5fa37a | ||
|
|
47bd5ba1ba | ||
|
|
b746ac0835 | ||
|
|
79989fb176 | ||
|
|
ecc7e224e9 | ||
|
|
549d219f44 | ||
|
|
ffe18db2fb | ||
|
|
e8b172f1c3 |
4
.github/workflows/backend-linter.yml
vendored
4
.github/workflows/backend-linter.yml
vendored
@@ -24,10 +24,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: backend/go.mod
|
go-version-file: backend/go.mod
|
||||||
|
|
||||||
|
|||||||
18
.github/workflows/build-next.yml
vendored
18
.github/workflows/build-next.yml
vendored
@@ -19,22 +19,20 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: 'backend/go.mod'
|
go-version-file: "backend/go.mod"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -74,7 +72,7 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:next
|
tags: ${{ env.DOCKER_IMAGE_NAME }}:next
|
||||||
file: Dockerfile-prebuilt
|
file: docker/Dockerfile-prebuilt
|
||||||
- name: Build and push container image (distroless)
|
- name: Build and push container image (distroless)
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
id: container-build-push-distroless
|
id: container-build-push-distroless
|
||||||
@@ -83,16 +81,16 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:next-distroless
|
tags: ${{ env.DOCKER_IMAGE_NAME }}:next-distroless
|
||||||
file: Dockerfile-distroless
|
file: docker/Dockerfile-distroless
|
||||||
- name: Container image attestation
|
- name: Container image attestation
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||||
subject-digest: ${{ steps.build-push-image.outputs.digest }}
|
subject-digest: ${{ steps.build-push-image.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
- name: Container image attestation (distroless)
|
- name: Container image attestation (distroless)
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||||
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|||||||
23
.github/workflows/e2e-tests.yml
vendored
23
.github/workflows/e2e-tests.yml
vendored
@@ -3,15 +3,15 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'docs/**'
|
- "docs/**"
|
||||||
- '**.md'
|
- "**.md"
|
||||||
- '.github/**'
|
- ".github/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'docs/**'
|
- "docs/**"
|
||||||
- '**.md'
|
- "**.md"
|
||||||
- '.github/**'
|
- ".github/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -30,6 +30,8 @@ jobs:
|
|||||||
- name: Build and export
|
- name: Build and export
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
push: false
|
push: false
|
||||||
load: false
|
load: false
|
||||||
tags: pocket-id:test
|
tags: pocket-id:test
|
||||||
@@ -57,16 +59,15 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
db: [sqlite, postgres]
|
db: [sqlite, postgres]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: Cache Playwright Browsers
|
- name: Cache Playwright Browsers
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
|||||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -3,7 +3,7 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "v*.*.*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -19,14 +19,12 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
- uses: actions/setup-go@v6
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
with:
|
||||||
go-version-file: 'backend/go.mod'
|
go-version-file: "backend/go.mod"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -81,7 +79,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
file: Dockerfile-prebuilt
|
file: docker/Dockerfile-prebuilt
|
||||||
- name: Build and push container image (distroless)
|
- name: Build and push container image (distroless)
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
id: container-build-push-distroless
|
id: container-build-push-distroless
|
||||||
@@ -91,21 +89,21 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-distroless.outputs.tags }}
|
tags: ${{ steps.meta-distroless.outputs.tags }}
|
||||||
labels: ${{ steps.meta-distroless.outputs.labels }}
|
labels: ${{ steps.meta-distroless.outputs.labels }}
|
||||||
file: Dockerfile-distroless
|
file: docker/Dockerfile-distroless
|
||||||
- name: Binary attestation
|
- name: Binary attestation
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-path: 'backend/.bin/pocket-id-**'
|
subject-path: "backend/.bin/pocket-id-**"
|
||||||
- name: Container image attestation
|
- name: Container image attestation
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||||
subject-digest: ${{ steps.container-build-push.outputs.digest }}
|
subject-digest: ${{ steps.container-build-push.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
- name: Container image attestation (distroless)
|
- name: Container image attestation (distroless)
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: '${{ env.DOCKER_IMAGE_NAME }}'
|
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||||
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
@@ -122,6 +120,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Mark release as published
|
- name: Mark release as published
|
||||||
run: gh release edit ${{ github.ref_name }} --draft=false
|
run: gh release edit ${{ github.ref_name }} --draft=false
|
||||||
|
|||||||
30
.github/workflows/svelte-check.yml
vendored
30
.github/workflows/svelte-check.yml
vendored
@@ -4,21 +4,21 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'frontend/src/**'
|
- "frontend/src/**"
|
||||||
- '.github/svelte-check-matcher.json'
|
- ".github/svelte-check-matcher.json"
|
||||||
- 'frontend/package.json'
|
- "frontend/package.json"
|
||||||
- 'frontend/package-lock.json'
|
- "frontend/package-lock.json"
|
||||||
- 'frontend/tsconfig.json'
|
- "frontend/tsconfig.json"
|
||||||
- 'frontend/svelte.config.js'
|
- "frontend/svelte.config.js"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'frontend/src/**'
|
- "frontend/src/**"
|
||||||
- '.github/svelte-check-matcher.json'
|
- ".github/svelte-check-matcher.json"
|
||||||
- 'frontend/package.json'
|
- "frontend/package.json"
|
||||||
- 'frontend/package-lock.json'
|
- "frontend/package-lock.json"
|
||||||
- 'frontend/tsconfig.json'
|
- "frontend/tsconfig.json"
|
||||||
- 'frontend/svelte.config.js'
|
- "frontend/svelte.config.js"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -34,17 +34,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
|
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||||
|
|||||||
4
.github/workflows/unit-tests.yml
vendored
4
.github/workflows/unit-tests.yml
vendored
@@ -16,8 +16,8 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: "backend/go.mod"
|
go-version-file: "backend/go.mod"
|
||||||
cache-dependency-path: "backend/go.sum"
|
cache-dependency-path: "backend/go.sum"
|
||||||
|
|||||||
2
.github/workflows/update-aaguids.yml
vendored
2
.github/workflows/update-aaguids.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Fetch JSON data
|
- name: Fetch JSON data
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,12 @@
|
|||||||
# JetBrains
|
# JetBrains
|
||||||
**/.idea
|
**/.idea
|
||||||
|
|
||||||
|
# Node
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# PNPM
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
927
CHANGELOG.md
927
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -32,10 +32,6 @@ func init() {
|
|||||||
panic(fmt.Errorf("failed to read index.html: %w", iErr))
|
panic(fmt.Errorf("failed to read index.html: %w", iErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the position of the first <script> tag
|
|
||||||
idx := bytes.Index(index, []byte(scriptTag))
|
|
||||||
|
|
||||||
// Create writeIndexFn, which adds the CSP tag to the script tag if needed
|
|
||||||
writeIndexFn = func(w io.Writer, nonce string) (err error) {
|
writeIndexFn = func(w io.Writer, nonce string) (err error) {
|
||||||
// If there's no nonce, write the index as-is
|
// If there's no nonce, write the index as-is
|
||||||
if nonce == "" {
|
if nonce == "" {
|
||||||
@@ -43,23 +39,16 @@ func init() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a nonce, so first write the index until the <script> tag
|
// Add nonce to all <script> tags
|
||||||
// Then we write the modified script tag
|
// We replace "<script" with `<script nonce="..."` everywhere it appears
|
||||||
// Finally, the rest of the index
|
modified := bytes.ReplaceAll(
|
||||||
_, err = w.Write(index[0:idx])
|
index,
|
||||||
if err != nil {
|
[]byte(scriptTag),
|
||||||
return err
|
[]byte(`<script nonce="`+nonce+`">`),
|
||||||
}
|
)
|
||||||
_, err = w.Write([]byte(`<script nonce="` + nonce + `">`))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = w.Write(index[(idx + len(scriptTag)):])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
_, err = w.Write(modified)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +64,11 @@ func RegisterFrontend(router *gin.Engine) error {
|
|||||||
router.NoRoute(func(c *gin.Context) {
|
router.NoRoute(func(c *gin.Context) {
|
||||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||||
|
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
c.Redirect(http.StatusMovedPermanently, strings.TrimRight(c.Request.URL.String(), "/"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(path, "api/") {
|
if strings.HasPrefix(path, "api/") {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
|
||||||
return
|
return
|
||||||
@@ -94,13 +88,9 @@ func RegisterFrontend(router *gin.Engine) error {
|
|||||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
c.Header("Cache-Control", "no-store")
|
c.Header("Cache-Control", "no-store")
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
|
if err := writeIndexFn(c.Writer, nonce); err != nil {
|
||||||
err = writeIndexFn(c.Writer, nonce)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
|
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
156
backend/go.mod
156
backend/go.mod
@@ -7,84 +7,87 @@ require (
|
|||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||||
github.com/emersion/go-smtp v0.21.3
|
github.com/emersion/go-smtp v0.24.0
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0
|
github.com/fxamacker/cbor/v2 v2.9.0
|
||||||
github.com/gin-contrib/slog v1.1.0
|
github.com/gin-contrib/slog v1.1.0
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/glebarez/go-sqlite v1.22.0
|
github.com/glebarez/go-sqlite v1.22.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3
|
github.com/go-co-op/gocron/v2 v2.17.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
github.com/go-webauthn/webauthn v0.11.2
|
github.com/go-webauthn/webauthn v0.14.0
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.3
|
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-uuid v1.0.3
|
github.com/hashicorp/go-uuid v1.0.3
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.0
|
github.com/lestrrat-go/httprc/v3 v3.0.1
|
||||||
github.com/lestrrat-go/jwx/v3 v3.0.10
|
github.com/lestrrat-go/jwx/v3 v3.0.12
|
||||||
github.com/lmittmann/tint v1.1.2
|
github.com/lmittmann/tint v1.1.2
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/orandin/slog-gorm v1.4.0
|
github.com/orandin/slog-gorm v1.4.0
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0
|
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0
|
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||||
go.opentelemetry.io/otel v1.37.0
|
go.opentelemetry.io/otel v1.38.0
|
||||||
go.opentelemetry.io/otel/log v0.13.0
|
go.opentelemetry.io/otel/log v0.14.0
|
||||||
go.opentelemetry.io/otel/metric v1.37.0
|
go.opentelemetry.io/otel/metric v1.38.0
|
||||||
go.opentelemetry.io/otel/sdk v1.35.0
|
go.opentelemetry.io/otel/sdk v1.38.0
|
||||||
go.opentelemetry.io/otel/sdk/log v0.10.0
|
go.opentelemetry.io/otel/sdk/log v0.14.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||||
go.opentelemetry.io/otel/trace v1.37.0
|
go.opentelemetry.io/otel/trace v1.38.0
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.43.0
|
||||||
golang.org/x/image v0.30.0
|
golang.org/x/image v0.32.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.17.0
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/text v0.30.0
|
||||||
golang.org/x/time v0.12.0
|
golang.org/x/time v0.14.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.30.1
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
github.com/disintegration/gift v1.1.2 // indirect
|
github.com/disintegration/gift v1.2.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-webauthn/x v0.1.23 // indirect
|
github.com/go-webauthn/x v0.1.25 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/google/go-github/v39 v39.2.0 // indirect
|
github.com/google/go-github/v39 v39.2.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/go-tpm v0.9.5 // indirect
|
github.com/google/go-tpm v0.9.6 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
@@ -93,57 +96,66 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.67.1 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.18.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/valyala/fastjson v1.6.4 // indirect
|
github.com/valyala/fastjson v1.6.4 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/net v0.43.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/oauth2 v0.27.0 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
golang.org/x/oauth2 v0.32.0 // indirect
|
||||||
google.golang.org/grpc v1.71.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.7 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||||
|
google.golang.org/grpc v1.76.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.66.7 // indirect
|
modernc.org/libc v1.66.10 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.2 // indirect
|
modernc.org/sqlite v1.39.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
176
backend/go.sum
176
backend/go.sum
@@ -6,10 +6,15 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/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 h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||||
|
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
@@ -30,8 +35,11 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw
|
|||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
|
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
|
||||||
github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
|
github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
|
||||||
|
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||||
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
||||||
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
|
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||||
|
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
@@ -40,6 +48,7 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
||||||
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -48,30 +57,44 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
|
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
|
||||||
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
|
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||||
|
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4=
|
github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4=
|
||||||
github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY=
|
github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -85,18 +108,30 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||||
|
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
|
||||||
|
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
|
||||||
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
|
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
|
||||||
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
|
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
|
||||||
|
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
|
||||||
|
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
|
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -112,6 +147,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
|||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
|
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||||
|
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
@@ -119,8 +156,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/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/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/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
|
||||||
|
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -137,6 +178,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
|
|||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
@@ -177,12 +220,20 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
|
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
|
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
|
||||||
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
|
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
|
||||||
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
|
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
|
||||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||||
@@ -195,6 +246,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
@@ -214,6 +267,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
@@ -222,6 +277,8 @@ github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsT
|
|||||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0 h1:Gyljxck1kHbBxDgLM++NfDWBqvu1pWWfT8XbosSo0bo=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -230,25 +287,47 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/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 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
|
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||||
|
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||||
|
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
|
||||||
|
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
|
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -261,6 +340,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
@@ -272,60 +353,116 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 h1:HY2hJ7yn3KuEBBBsKxvF3ViSmzLwsgeNvD+0utRMgzc=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0 h1:HY2hJ7yn3KuEBBBsKxvF3ViSmzLwsgeNvD+0utRMgzc=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0/go.mod h1:H4H7vs8766kwFnOZVEGMJFVF+phpBSmTckvvNRdJeDI=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.59.0/go.mod h1:H4H7vs8766kwFnOZVEGMJFVF+phpBSmTckvvNRdJeDI=
|
||||||
|
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
|
||||||
|
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0 h1:dKhAFwh7SSoOw+gwMtSv+XLkUGTFAwAGMT3X3XSE4FA=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0/go.mod h1:fPl+qlrhRdRntIpPs9JoQ0iBKAsnH5VkgppU1f9kyF4=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.59.0/go.mod h1:fPl+qlrhRdRntIpPs9JoQ0iBKAsnH5VkgppU1f9kyF4=
|
||||||
|
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
|
||||||
|
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g=
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4=
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4=
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 h1:5kSIJ0y8ckZZKoDhZHdVtcyjVi6rXyAwyaR8mp4zLbg=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0/go.mod h1:i+fIMHvcSQtsIY82/xgiVWRklrNt/O6QriHLjzGeY+s=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
|
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
|
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
||||||
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
|
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
|
||||||
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
|
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
|
||||||
|
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
||||||
|
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw=
|
go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo=
|
go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
@@ -336,11 +473,17 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||||
|
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||||
|
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
@@ -348,6 +491,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -362,9 +507,13 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
|
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
|
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -374,6 +523,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -388,6 +539,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -409,8 +562,12 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@@ -419,17 +576,27 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
|
|||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||||
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -441,18 +608,25 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
|||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||||
|
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||||
|
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -463,6 +637,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
|
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
||||||
|
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -140,6 +141,7 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
var dialector gorm.Dialector
|
var dialector gorm.Dialector
|
||||||
|
|
||||||
// Choose the correct database provider
|
// Choose the correct database provider
|
||||||
|
var onConnFn func(conn *sql.DB)
|
||||||
switch common.EnvConfig.DbProvider {
|
switch common.EnvConfig.DbProvider {
|
||||||
case common.DbProviderSqlite:
|
case common.DbProviderSqlite:
|
||||||
if common.EnvConfig.DbConnectionString == "" {
|
if common.EnvConfig.DbConnectionString == "" {
|
||||||
@@ -148,7 +150,7 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
|
|
||||||
sqliteutil.RegisterSqliteFunctions()
|
sqliteutil.RegisterSqliteFunctions()
|
||||||
|
|
||||||
connString, dbPath, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
|
connString, dbPath, isMemoryDB, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -159,6 +161,14 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isMemoryDB {
|
||||||
|
// For in-memory SQLite databases, we must limit to 1 open connection at the same time, or they won't see the whole data
|
||||||
|
// The other workaround, of using shared caches, doesn't work well with multiple write transactions trying to happen at once
|
||||||
|
onConnFn = func(conn *sql.DB) {
|
||||||
|
conn.SetMaxOpenConns(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dialector = sqlite.Open(connString)
|
dialector = sqlite.Open(connString)
|
||||||
case common.DbProviderPostgres:
|
case common.DbProviderPostgres:
|
||||||
if common.EnvConfig.DbConnectionString == "" {
|
if common.EnvConfig.DbConnectionString == "" {
|
||||||
@@ -176,6 +186,16 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
slog.Info("Connected to database", slog.String("provider", string(common.EnvConfig.DbProvider)))
|
slog.Info("Connected to database", slog.String("provider", string(common.EnvConfig.DbProvider)))
|
||||||
|
|
||||||
|
if onConnFn != nil {
|
||||||
|
conn, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to get database connection, will retry in 3s", slog.Int("attempt", i), slog.String("provider", string(common.EnvConfig.DbProvider)), slog.Any("error", err))
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
onConnFn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,18 +208,18 @@ func connectDatabase() (db *gorm.DB, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSqliteConnectionString(connString string) (parsedConnString string, dbPath string, err error) {
|
func parseSqliteConnectionString(connString string) (parsedConnString string, dbPath string, isMemoryDB bool, err error) {
|
||||||
if !strings.HasPrefix(connString, "file:") {
|
if !strings.HasPrefix(connString, "file:") {
|
||||||
connString = "file:" + connString
|
connString = "file:" + connString
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're using an in-memory database
|
// Check if we're using an in-memory database
|
||||||
isMemoryDB := isSqliteInMemory(connString)
|
isMemoryDB = isSqliteInMemory(connString)
|
||||||
|
|
||||||
// Parse the connection string
|
// Parse the connection string
|
||||||
connStringUrl, err := url.Parse(connString)
|
connStringUrl, err := url.Parse(connString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
|
return "", "", false, fmt.Errorf("failed to parse SQLite connection string: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert options for the old SQLite driver to the new one
|
// Convert options for the old SQLite driver to the new one
|
||||||
@@ -208,7 +228,7 @@ func parseSqliteConnectionString(connString string) (parsedConnString string, db
|
|||||||
// Add the default and required params
|
// Add the default and required params
|
||||||
err = addSqliteDefaultParameters(connStringUrl, isMemoryDB)
|
err = addSqliteDefaultParameters(connStringUrl, isMemoryDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("invalid SQLite connection string: %w", err)
|
return "", "", false, fmt.Errorf("invalid SQLite connection string: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the absolute path to the database
|
// Get the absolute path to the database
|
||||||
@@ -217,10 +237,10 @@ func parseSqliteConnectionString(connString string) (parsedConnString string, db
|
|||||||
idx := strings.IndexRune(parsedConnString, '?')
|
idx := strings.IndexRune(parsedConnString, '?')
|
||||||
dbPath, err = filepath.Abs(parsedConnString[len("file:"):idx])
|
dbPath, err = filepath.Abs(parsedConnString[len("file:"):idx])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("failed to determine absolute path to the database: %w", err)
|
return "", "", false, fmt.Errorf("failed to determine absolute path to the database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedConnString, dbPath, nil
|
return parsedConnString, dbPath, isMemoryDB, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The official C implementation of SQLite allows some additional properties in the connection string
|
// The official C implementation of SQLite allows some additional properties in the connection string
|
||||||
@@ -272,11 +292,6 @@ func addSqliteDefaultParameters(connStringUrl *url.URL, isMemoryDB bool) error {
|
|||||||
qs = make(url.Values, 2)
|
qs = make(url.Values, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the database is in-memory, we must ensure that cache=shared is set
|
|
||||||
if isMemoryDB {
|
|
||||||
qs["cache"] = []string{"shared"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the database is read-only or immutable
|
// Check if the database is read-only or immutable
|
||||||
isReadOnly := false
|
isReadOnly := false
|
||||||
if len(qs["mode"]) > 0 {
|
if len(qs["mode"]) > 0 {
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ func TestAddSqliteDefaultParameters(t *testing.T) {
|
|||||||
name: "in-memory database",
|
name: "in-memory database",
|
||||||
input: "file::memory:",
|
input: "file::memory:",
|
||||||
isMemoryDB: true,
|
isMemoryDB: true,
|
||||||
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate&cache=shared",
|
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "read-only database with mode=ro",
|
name: "read-only database with mode=ro",
|
||||||
@@ -249,12 +249,6 @@ func TestAddSqliteDefaultParameters(t *testing.T) {
|
|||||||
isMemoryDB: false,
|
isMemoryDB: false,
|
||||||
expected: "file:test.db?_pragma=busy_timeout%283000%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28TRUNCATE%29&_pragma=synchronous%28NORMAL%29&_txlock=immediate",
|
expected: "file:test.db?_pragma=busy_timeout%283000%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28TRUNCATE%29&_pragma=synchronous%28NORMAL%29&_txlock=immediate",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "in-memory database with cache already set",
|
|
||||||
input: "file::memory:?cache=private",
|
|
||||||
isMemoryDB: true,
|
|
||||||
expected: "file::memory:?_pragma=busy_timeout%282500%29&_pragma=foreign_keys%281%29&_pragma=journal_mode%28MEMORY%29&_txlock=immediate&cache=shared",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "database with mode=rw (not read-only)",
|
name: "database with mode=rw (not read-only)",
|
||||||
input: "file:test.db?mode=rw",
|
input: "file:test.db?mode=rw",
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
|||||||
if common.EnvConfig.UnixSocket != "" {
|
if common.EnvConfig.UnixSocket != "" {
|
||||||
network = "unix"
|
network = "unix"
|
||||||
addr = common.EnvConfig.UnixSocket
|
addr = common.EnvConfig.UnixSocket
|
||||||
|
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen(network, addr) //nolint:noctx
|
listener, err := net.Listen(network, addr) //nolint:noctx
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima
|
|||||||
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
|
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService)
|
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
|
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
|
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
|
||||||
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService)
|
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService)
|
||||||
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
|
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService)
|
||||||
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
|
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -34,7 +35,7 @@ const (
|
|||||||
type EnvConfigSchema struct {
|
type EnvConfigSchema struct {
|
||||||
AppEnv string `env:"APP_ENV" options:"toLower"`
|
AppEnv string `env:"APP_ENV" options:"toLower"`
|
||||||
LogLevel string `env:"LOG_LEVEL" options:"toLower"`
|
LogLevel string `env:"LOG_LEVEL" options:"toLower"`
|
||||||
AppURL string `env:"APP_URL" options:"toLower"`
|
AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"`
|
||||||
DbProvider DbProvider `env:"DB_PROVIDER" options:"toLower"`
|
DbProvider DbProvider `env:"DB_PROVIDER" options:"toLower"`
|
||||||
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
|
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
|
||||||
UploadPath string `env:"UPLOAD_PATH"`
|
UploadPath string `env:"UPLOAD_PATH"`
|
||||||
@@ -180,6 +181,25 @@ func validateEnvConfig(config *EnvConfigSchema) error {
|
|||||||
return fmt.Errorf("invalid value for KEYS_STORAGE: %s", config.KeysStorage)
|
return fmt.Errorf("invalid value for KEYS_STORAGE: %s", config.KeysStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate LOCAL_IPV6_RANGES
|
||||||
|
ranges := strings.Split(config.LocalIPv6Ranges, ",")
|
||||||
|
for _, rangeStr := range ranges {
|
||||||
|
rangeStr = strings.TrimSpace(rangeStr)
|
||||||
|
if rangeStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid LOCAL_IPV6_RANGES '%s': %w", rangeStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipNet.IP.To4() != nil {
|
||||||
|
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -207,6 +227,10 @@ func prepareEnvConfig(config *EnvConfigSchema) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "trimTrailingSlash":
|
||||||
|
if field.Kind() == reflect.String {
|
||||||
|
field.SetString(strings.TrimRight(field.String(), "/"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,3 +378,23 @@ func (e *ClientIdAlreadyExistsError) Error() string {
|
|||||||
func (e *ClientIdAlreadyExistsError) HttpStatusCode() int {
|
func (e *ClientIdAlreadyExistsError) HttpStatusCode() int {
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserEmailNotSetError struct{}
|
||||||
|
|
||||||
|
func (e *UserEmailNotSetError) Error() string {
|
||||||
|
return "The user does not have an email address set"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEmailNotSetError) HttpStatusCode() int {
|
||||||
|
return http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageNotFoundError struct{}
|
||||||
|
|
||||||
|
func (e *ImageNotFoundError) Error() string {
|
||||||
|
return "Image not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ImageNotFoundError) HttpStatusCode() int {
|
||||||
|
return http.StatusNotFound
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,15 +45,11 @@ func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.Auth
|
|||||||
// @Success 200 {object} dto.Paginated[dto.ApiKeyDto]
|
// @Success 200 {object} dto.Paginated[dto.ApiKeyDto]
|
||||||
// @Router /api/api-keys [get]
|
// @Router /api/api-keys [get]
|
||||||
func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
|
func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
|
||||||
|
listRequestOptions := utils.ParseListRequestOptions(ctx)
|
||||||
|
|
||||||
userID := ctx.GetString("userID")
|
userID := ctx.GetString("userID")
|
||||||
|
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
apiKeys, pagination, err := c.apiKeyService.ListApiKeys(ctx.Request.Context(), userID, listRequestOptions)
|
||||||
if err := ctx.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKeys, pagination, err := c.apiKeyService.ListApiKeys(ctx.Request.Context(), userID, sortedPaginationRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = ctx.Error(err)
|
_ = ctx.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -25,10 +25,14 @@ func NewAppImagesController(
|
|||||||
group.GET("/application-images/logo", controller.getLogoHandler)
|
group.GET("/application-images/logo", controller.getLogoHandler)
|
||||||
group.GET("/application-images/background", controller.getBackgroundImageHandler)
|
group.GET("/application-images/background", controller.getBackgroundImageHandler)
|
||||||
group.GET("/application-images/favicon", controller.getFaviconHandler)
|
group.GET("/application-images/favicon", controller.getFaviconHandler)
|
||||||
|
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
|
||||||
|
|
||||||
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
|
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
|
||||||
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
|
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
|
||||||
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
|
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
|
||||||
|
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
|
||||||
|
|
||||||
|
group.DELETE("/application-images/default-profile-picture", authMiddleware.Add(), controller.deleteDefaultProfilePicture)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppImagesController struct {
|
type AppImagesController struct {
|
||||||
@@ -78,6 +82,18 @@ func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) {
|
|||||||
c.getImage(ctx, "favicon")
|
c.getImage(ctx, "favicon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultProfilePicture godoc
|
||||||
|
// @Summary Get default profile picture image
|
||||||
|
// @Description Get the default profile picture image for the application
|
||||||
|
// @Tags Application Images
|
||||||
|
// @Produce image/png
|
||||||
|
// @Produce image/jpeg
|
||||||
|
// @Success 200 {file} binary "Default profile picture image"
|
||||||
|
// @Router /api/application-images/default-profile-picture [get]
|
||||||
|
func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) {
|
||||||
|
c.getImage(ctx, "default-profile-picture")
|
||||||
|
}
|
||||||
|
|
||||||
// updateLogoHandler godoc
|
// updateLogoHandler godoc
|
||||||
// @Summary Update logo
|
// @Summary Update logo
|
||||||
// @Description Update the application logo
|
// @Description Update the application logo
|
||||||
@@ -171,3 +187,41 @@ func (c *AppImagesController) getImage(ctx *gin.Context, name string) {
|
|||||||
utils.SetCacheControlHeader(ctx, 15*time.Minute, 24*time.Hour)
|
utils.SetCacheControlHeader(ctx, 15*time.Minute, 24*time.Hour)
|
||||||
ctx.File(imagePath)
|
ctx.File(imagePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateDefaultProfilePicture godoc
|
||||||
|
// @Summary Update default profile picture image
|
||||||
|
// @Description Update the default profile picture image
|
||||||
|
// @Tags Application Images
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param file formData file true "Profile picture image file"
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Router /api/application-images/default-profile-picture [put]
|
||||||
|
func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) {
|
||||||
|
file, err := ctx.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
_ = ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.appImagesService.UpdateImage(file, "default-profile-picture"); err != nil {
|
||||||
|
_ = ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteDefaultProfilePicture godoc
|
||||||
|
// @Summary Delete default profile picture image
|
||||||
|
// @Description Delete the default profile picture image
|
||||||
|
// @Tags Application Images
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Router /api/application-images/default-profile-picture [delete]
|
||||||
|
func (c *AppImagesController) deleteDefaultProfilePicture(ctx *gin.Context) {
|
||||||
|
if err := c.appImagesService.DeleteImage("default-profile-picture"); err != nil {
|
||||||
|
_ = ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,18 +41,12 @@ type AuditLogController struct {
|
|||||||
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
||||||
// @Router /api/audit-logs [get]
|
// @Router /api/audit-logs [get]
|
||||||
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
|
|
||||||
err := c.ShouldBindQuery(&sortedPaginationRequest)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
// Fetch audit logs for the user
|
// Fetch audit logs for the user
|
||||||
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, sortedPaginationRequest)
|
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, listRequestOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -86,26 +80,12 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
|||||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||||
// @Param sort[column] query string false "Column to sort by"
|
// @Param sort[column] query string false "Column to sort by"
|
||||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||||
// @Param filters[userId] query string false "Filter by user ID"
|
|
||||||
// @Param filters[event] query string false "Filter by event type"
|
|
||||||
// @Param filters[clientName] query string false "Filter by client name"
|
|
||||||
// @Param filters[location] query string false "Filter by location type (external or internal)"
|
|
||||||
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
||||||
// @Router /api/audit-logs/all [get]
|
// @Router /api/audit-logs/all [get]
|
||||||
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var filters dto.AuditLogFilterDto
|
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), listRequestOptions)
|
||||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), sortedPaginationRequest, filters)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -357,6 +358,7 @@ func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
|
|||||||
clientDto := dto.OidcClientMetaDataDto{}
|
clientDto := dto.OidcClientMetaDataDto{}
|
||||||
err = dto.MapStruct(client, &clientDto)
|
err = dto.MapStruct(client, &clientDto)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
clientDto.HasDarkLogo = client.HasDarkLogo()
|
||||||
c.JSON(http.StatusOK, clientDto)
|
c.JSON(http.StatusOK, clientDto)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -403,13 +405,9 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
|||||||
// @Router /api/oidc/clients [get]
|
// @Router /api/oidc/clients [get]
|
||||||
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, sortedPaginationRequest)
|
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, listRequestOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -423,6 +421,7 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
|||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
clientDto.HasDarkLogo = client.HasDarkLogo()
|
||||||
clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID)
|
clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
@@ -543,10 +542,13 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
|||||||
// @Produce image/jpeg
|
// @Produce image/jpeg
|
||||||
// @Produce image/svg+xml
|
// @Produce image/svg+xml
|
||||||
// @Param id path string true "Client ID"
|
// @Param id path string true "Client ID"
|
||||||
|
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||||
// @Success 200 {file} binary "Logo image"
|
// @Success 200 {file} binary "Logo image"
|
||||||
// @Router /api/oidc/clients/{id}/logo [get]
|
// @Router /api/oidc/clients/{id}/logo [get]
|
||||||
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||||
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"))
|
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||||
|
|
||||||
|
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -565,6 +567,7 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
|||||||
// @Accept multipart/form-data
|
// @Accept multipart/form-data
|
||||||
// @Param id path string true "Client ID"
|
// @Param id path string true "Client ID"
|
||||||
// @Param file formData file true "Logo image file (PNG, JPG, or SVG)"
|
// @Param file formData file true "Logo image file (PNG, JPG, or SVG)"
|
||||||
|
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||||
// @Success 204 "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Router /api/oidc/clients/{id}/logo [post]
|
// @Router /api/oidc/clients/{id}/logo [post]
|
||||||
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||||
@@ -574,13 +577,16 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file)
|
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||||
|
|
||||||
|
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file, lightLogo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteClientLogoHandler godoc
|
// deleteClientLogoHandler godoc
|
||||||
@@ -588,16 +594,26 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
|||||||
// @Description Delete the logo for an OIDC client
|
// @Description Delete the logo for an OIDC client
|
||||||
// @Tags OIDC
|
// @Tags OIDC
|
||||||
// @Param id path string true "Client ID"
|
// @Param id path string true "Client ID"
|
||||||
|
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||||
// @Success 204 "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Router /api/oidc/clients/{id}/logo [delete]
|
// @Router /api/oidc/clients/{id}/logo [delete]
|
||||||
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
||||||
err := oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id"))
|
var err error
|
||||||
|
|
||||||
|
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||||
|
if lightLogo {
|
||||||
|
err = oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id"))
|
||||||
|
} else {
|
||||||
|
err = oc.oidcService.DeleteClientDarkLogo(c.Request.Context(), c.Param("id"))
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateAllowedUserGroupsHandler godoc
|
// updateAllowedUserGroupsHandler godoc
|
||||||
@@ -628,6 +644,7 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
|
|||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
oidcClientDto.HasDarkLogo = oidcClient.HasDarkLogo()
|
||||||
|
|
||||||
c.JSON(http.StatusOK, oidcClientDto)
|
c.JSON(http.StatusOK, oidcClientDto)
|
||||||
}
|
}
|
||||||
@@ -685,12 +702,9 @@ func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
|
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, listRequestOptions)
|
||||||
return
|
|
||||||
}
|
|
||||||
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, sortedPaginationRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -741,15 +755,11 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
|
|||||||
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
|
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
|
||||||
// @Router /api/oidc/users/me/clients [get]
|
// @Router /api/oidc/users/me/clients [get]
|
||||||
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
|
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
|
||||||
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
|
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, listRequestOptions)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, sortedPaginationRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -104,13 +104,9 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
|||||||
// @Router /api/users [get]
|
// @Router /api/users [get]
|
||||||
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, sortedPaginationRequest)
|
users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, listRequestOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -574,13 +570,9 @@ func (uc *UserController) createSignupTokenHandler(c *gin.Context) {
|
|||||||
// @Success 200 {object} dto.Paginated[dto.SignupTokenDto]
|
// @Success 200 {object} dto.Paginated[dto.SignupTokenDto]
|
||||||
// @Router /api/signup-tokens [get]
|
// @Router /api/signup-tokens [get]
|
||||||
func (uc *UserController) listSignupTokensHandler(c *gin.Context) {
|
func (uc *UserController) listSignupTokensHandler(c *gin.Context) {
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, pagination, err := uc.userService.ListSignupTokens(c.Request.Context(), sortedPaginationRequest)
|
tokens, pagination, err := uc.userService.ListSignupTokens(c.Request.Context(), listRequestOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -47,16 +47,10 @@ type UserGroupController struct {
|
|||||||
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
|
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
|
||||||
// @Router /api/user-groups [get]
|
// @Router /api/user-groups [get]
|
||||||
func (ugc *UserGroupController) list(c *gin.Context) {
|
func (ugc *UserGroupController) list(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
|
||||||
_ = c.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
groups, pagination, err := ugc.UserGroupService.List(ctx, searchTerm, sortedPaginationRequest)
|
groups, pagination, err := ugc.UserGroupService.List(c, searchTerm, listRequestOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -70,7 +64,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
|||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(ctx, group.ID)
|
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(c.Request.Context(), group.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type AppConfigUpdateDto struct {
|
|||||||
SignupDefaultUserGroupIDs string `json:"signupDefaultUserGroupIDs" binding:"omitempty,json"`
|
SignupDefaultUserGroupIDs string `json:"signupDefaultUserGroupIDs" binding:"omitempty,json"`
|
||||||
SignupDefaultCustomClaims string `json:"signupDefaultCustomClaims" binding:"omitempty,json"`
|
SignupDefaultCustomClaims string `json:"signupDefaultCustomClaims" binding:"omitempty,json"`
|
||||||
AccentColor string `json:"accentColor"`
|
AccentColor string `json:"accentColor"`
|
||||||
|
RequireUserEmail string `json:"requireUserEmail" binding:"required"`
|
||||||
SmtpHost string `json:"smtpHost"`
|
SmtpHost string `json:"smtpHost"`
|
||||||
SmtpPort string `json:"smtpPort"`
|
SmtpPort string `json:"smtpPort"`
|
||||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||||
|
|||||||
@@ -17,10 +17,3 @@ type AuditLogDto struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Data map[string]string `json:"data"`
|
Data map[string]string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuditLogFilterDto struct {
|
|
||||||
UserID string `form:"filters[userId]"`
|
|
||||||
Event string `form:"filters[event]"`
|
|
||||||
ClientName string `form:"filters[clientName]"`
|
|
||||||
Location string `form:"filters[location]"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type OidcClientMetaDataDto struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HasLogo bool `json:"hasLogo"`
|
HasLogo bool `json:"hasLogo"`
|
||||||
|
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||||
LaunchURL *string `json:"launchURL"`
|
LaunchURL *string `json:"launchURL"`
|
||||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,10 @@ type OidcClientUpdateDto struct {
|
|||||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
||||||
|
HasLogo bool `json:"hasLogo"`
|
||||||
|
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||||
|
LogoURL *string `json:"logoUrl"`
|
||||||
|
DarkLogoURL *string `json:"darkLogoUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientCreateDto struct {
|
type OidcClientCreateDto struct {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
type UserDto struct {
|
type UserDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email" `
|
Email *string `json:"email" `
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName *string `json:"lastName"`
|
LastName *string `json:"lastName"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
@@ -24,10 +24,10 @@ type UserDto struct {
|
|||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
||||||
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
||||||
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
||||||
DisplayName string `json:"displayName" binding:"required,max=100" unorm:"nfc"`
|
DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
@@ -64,9 +64,9 @@ type UserUpdateUserGroupDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SignUpDto struct {
|
type SignUpDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
||||||
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
||||||
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dto
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
name: "valid input",
|
name: "valid input",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: utils.Ptr("test@example.com"),
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
DisplayName: "John Doe",
|
||||||
@@ -26,7 +27,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "missing username",
|
name: "missing username",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Email: "test@example.com",
|
Email: utils.Ptr("test@example.com"),
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
DisplayName: "John Doe",
|
||||||
@@ -36,7 +37,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "missing display name",
|
name: "missing display name",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Email: "test@example.com",
|
Email: utils.Ptr("test@example.com"),
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
},
|
},
|
||||||
@@ -46,7 +47,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
name: "username contains invalid characters",
|
name: "username contains invalid characters",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "test/ser",
|
Username: "test/ser",
|
||||||
Email: "test@example.com",
|
Email: utils.Ptr("test@example.com"),
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
DisplayName: "John Doe",
|
||||||
@@ -57,7 +58,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
name: "invalid email",
|
name: "invalid email",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "not-an-email",
|
Email: utils.Ptr("not-an-email"),
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
DisplayName: "John Doe",
|
||||||
@@ -68,7 +69,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
name: "first name too short",
|
name: "first name too short",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: utils.Ptr("test@example.com"),
|
||||||
FirstName: "",
|
FirstName: "",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
DisplayName: "John Doe",
|
||||||
@@ -79,7 +80,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
name: "last name too long",
|
name: "last name too long",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: utils.Ptr("test@example.com"),
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
|
LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
|
||||||
DisplayName: "John Doe",
|
DisplayName: "John Doe",
|
||||||
|
|||||||
@@ -67,14 +67,12 @@ func ValidateClientID(clientID string) bool {
|
|||||||
|
|
||||||
// ValidateCallbackURL validates callback URLs with support for wildcards
|
// ValidateCallbackURL validates callback URLs with support for wildcards
|
||||||
func ValidateCallbackURL(raw string) bool {
|
func ValidateCallbackURL(raw string) bool {
|
||||||
if raw == "*" {
|
// Don't validate if it contains a wildcard
|
||||||
|
if strings.Contains(raw, "*") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace all '*' with 'x' to check if the rest is still a valid URI
|
u, err := url.Parse(raw)
|
||||||
test := strings.ReplaceAll(raw, "*", "x")
|
|
||||||
|
|
||||||
u, err := url.Parse(test)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range apiKeys {
|
for _, key := range apiKeys {
|
||||||
if key.User.Email == "" {
|
if key.User.Email == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = j.apiKeyService.SendApiKeyExpiringSoonEmail(ctx, key)
|
err = j.apiKeyService.SendApiKeyExpiringSoonEmail(ctx, key)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (m *CspMiddleware) Add() gin.HandlerFunc {
|
|||||||
"object-src 'none'; " +
|
"object-src 'none'; " +
|
||||||
"frame-ancestors 'none'; " +
|
"frame-ancestors 'none'; " +
|
||||||
"form-action 'self'; " +
|
"form-action 'self'; " +
|
||||||
"img-src 'self' data: blob:; " +
|
"img-src * blob:;" +
|
||||||
"font-src 'self'; " +
|
"font-src 'self'; " +
|
||||||
"style-src 'self' 'unsafe-inline'; " +
|
"style-src 'self' 'unsafe-inline'; " +
|
||||||
"script-src 'self' 'nonce-" + nonce + "'"
|
"script-src 'self' 'nonce-" + nonce + "'"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type AppConfig struct {
|
|||||||
// Internal
|
// Internal
|
||||||
InstanceID AppConfigVariable `key:"instanceId,internal"` // Internal
|
InstanceID AppConfigVariable `key:"instanceId,internal"` // Internal
|
||||||
// Email
|
// Email
|
||||||
|
RequireUserEmail AppConfigVariable `key:"requireUserEmail,public"` // Public
|
||||||
SmtpHost AppConfigVariable `key:"smtpHost"`
|
SmtpHost AppConfigVariable `key:"smtpHost"`
|
||||||
SmtpPort AppConfigVariable `key:"smtpPort"`
|
SmtpPort AppConfigVariable `key:"smtpPort"`
|
||||||
SmtpFrom AppConfigVariable `key:"smtpFrom"`
|
SmtpFrom AppConfigVariable `key:"smtpFrom"`
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package model
|
|||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuditLog struct {
|
type AuditLog struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Event AuditLogEvent `sortable:"true"`
|
Event AuditLogEvent `sortable:"true" filterable:"true"`
|
||||||
IpAddress *string `sortable:"true"`
|
IpAddress *string `sortable:"true"`
|
||||||
Country string `sortable:"true"`
|
Country string `sortable:"true"`
|
||||||
City string `sortable:"true"`
|
City string `sortable:"true"`
|
||||||
@@ -17,7 +18,7 @@ type AuditLog struct {
|
|||||||
Username string `gorm:"-"`
|
Username string `gorm:"-"`
|
||||||
Data AuditLogData
|
Data AuditLogData
|
||||||
|
|
||||||
UserID string
|
UserID string `filterable:"true"`
|
||||||
User User
|
User User
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,14 +48,7 @@ func (e AuditLogEvent) Value() (driver.Value, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AuditLogData) Scan(value any) error {
|
func (d *AuditLogData) Scan(value any) error {
|
||||||
switch v := value.(type) {
|
return utils.UnmarshalJSONFromDatabase(d, value)
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(v, d)
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(v), d)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type: %T", value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d AuditLogData) Value() (driver.Value, error) {
|
func (d AuditLogData) Value() (driver.Value, error) {
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package model
|
|||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserAuthorizedOidcClient struct {
|
type UserAuthorizedOidcClient struct {
|
||||||
@@ -54,10 +52,10 @@ type OidcClient struct {
|
|||||||
CallbackURLs UrlList
|
CallbackURLs UrlList
|
||||||
LogoutCallbackURLs UrlList
|
LogoutCallbackURLs UrlList
|
||||||
ImageType *string
|
ImageType *string
|
||||||
HasLogo bool `gorm:"-"`
|
DarkImageType *string
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
PkceEnabled bool
|
PkceEnabled bool `sortable:"true" filterable:"true"`
|
||||||
RequiresReauthentication bool
|
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
||||||
Credentials OidcClientCredentials
|
Credentials OidcClientCredentials
|
||||||
LaunchURL *string
|
LaunchURL *string
|
||||||
|
|
||||||
@@ -67,6 +65,14 @@ type OidcClient struct {
|
|||||||
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
|
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c OidcClient) HasLogo() bool {
|
||||||
|
return c.ImageType != nil && *c.ImageType != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c OidcClient) HasDarkLogo() bool {
|
||||||
|
return c.DarkImageType != nil && *c.DarkImageType != ""
|
||||||
|
}
|
||||||
|
|
||||||
type OidcRefreshToken struct {
|
type OidcRefreshToken struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
@@ -89,12 +95,6 @@ func (c OidcRefreshToken) Scopes() []string {
|
|||||||
return strings.Split(c.Scope, " ")
|
return strings.Split(c.Scope, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) {
|
|
||||||
// Compute HasLogo field
|
|
||||||
c.HasLogo = c.ImageType != nil && *c.ImageType != ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcClientCredentials struct { //nolint:recvcheck
|
type OidcClientCredentials struct { //nolint:recvcheck
|
||||||
FederatedIdentities []OidcClientFederatedIdentity `json:"federatedIdentities,omitempty"`
|
FederatedIdentities []OidcClientFederatedIdentity `json:"federatedIdentities,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -121,14 +121,7 @@ func (occ OidcClientCredentials) FederatedIdentityForIssuer(issuer string) (Oidc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (occ *OidcClientCredentials) Scan(value any) error {
|
func (occ *OidcClientCredentials) Scan(value any) error {
|
||||||
switch v := value.(type) {
|
return utils.UnmarshalJSONFromDatabase(occ, value)
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(v, occ)
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(v), occ)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type: %T", value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (occ OidcClientCredentials) Value() (driver.Value, error) {
|
func (occ OidcClientCredentials) Value() (driver.Value, error) {
|
||||||
@@ -138,14 +131,7 @@ func (occ OidcClientCredentials) Value() (driver.Value, error) {
|
|||||||
type UrlList []string //nolint:recvcheck
|
type UrlList []string //nolint:recvcheck
|
||||||
|
|
||||||
func (cu *UrlList) Scan(value any) error {
|
func (cu *UrlList) Scan(value any) error {
|
||||||
switch v := value.(type) {
|
return utils.UnmarshalJSONFromDatabase(cu, value)
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(v, cu)
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(v), cu)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type: %T", value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cu UrlList) Value() (driver.Value, error) {
|
func (cu UrlList) Value() (driver.Value, error) {
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Username string `sortable:"true"`
|
Username string `sortable:"true"`
|
||||||
Email string `sortable:"true"`
|
Email *string `sortable:"true"`
|
||||||
FirstName string `sortable:"true"`
|
FirstName string `sortable:"true"`
|
||||||
LastName string `sortable:"true"`
|
LastName string `sortable:"true"`
|
||||||
DisplayName string `sortable:"true"`
|
DisplayName string `sortable:"true"`
|
||||||
IsAdmin bool `sortable:"true"`
|
IsAdmin bool `sortable:"true" filterable:"true"`
|
||||||
Locale *string
|
Locale *string
|
||||||
LdapID *string
|
LdapID *string
|
||||||
Disabled bool `sortable:"true"`
|
Disabled bool `sortable:"true" filterable:"true"`
|
||||||
|
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package model
|
|||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebauthnSession struct {
|
type WebauthnSession struct {
|
||||||
@@ -16,6 +16,7 @@ type WebauthnSession struct {
|
|||||||
Challenge string
|
Challenge string
|
||||||
ExpiresAt datatype.DateTime
|
ExpiresAt datatype.DateTime
|
||||||
UserVerification string
|
UserVerification string
|
||||||
|
CredentialParams CredentialParameters
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebauthnCredential struct {
|
type WebauthnCredential struct {
|
||||||
@@ -58,16 +59,20 @@ type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvc
|
|||||||
|
|
||||||
// Scan and Value methods for GORM to handle the custom type
|
// Scan and Value methods for GORM to handle the custom type
|
||||||
func (atl *AuthenticatorTransportList) Scan(value interface{}) error {
|
func (atl *AuthenticatorTransportList) Scan(value interface{}) error {
|
||||||
switch v := value.(type) {
|
return utils.UnmarshalJSONFromDatabase(atl, value)
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(v, atl)
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(v), atl)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type: %T", value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (atl AuthenticatorTransportList) Value() (driver.Value, error) {
|
func (atl AuthenticatorTransportList) Value() (driver.Value, error) {
|
||||||
return json.Marshal(atl)
|
return json.Marshal(atl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CredentialParameters []protocol.CredentialParameter //nolint:recvcheck
|
||||||
|
|
||||||
|
// Scan and Value methods for GORM to handle the custom type
|
||||||
|
func (cp *CredentialParameters) Scan(value interface{}) error {
|
||||||
|
return utils.UnmarshalJSONFromDatabase(cp, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp CredentialParameters) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(cp)
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ func NewApiKeyService(db *gorm.DB, emailService *EmailService) *ApiKeyService {
|
|||||||
return &ApiKeyService{db: db, emailService: emailService}
|
return &ApiKeyService{db: db, emailService: emailService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.ApiKey, utils.PaginationResponse, error) {
|
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.ApiKey, utils.PaginationResponse, error) {
|
||||||
query := s.db.
|
query := s.db.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Where("user_id = ?", userID).
|
Where("user_id = ?", userID).
|
||||||
Model(&model.ApiKey{})
|
Model(&model.ApiKey{})
|
||||||
|
|
||||||
var apiKeys []model.ApiKey
|
var apiKeys []model.ApiKey
|
||||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &apiKeys)
|
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &apiKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, utils.PaginationResponse{}, err
|
return nil, utils.PaginationResponse{}, err
|
||||||
}
|
}
|
||||||
@@ -144,9 +144,13 @@ func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Email == nil {
|
||||||
|
return &common.UserEmailNotSetError{}
|
||||||
|
}
|
||||||
|
|
||||||
err := SendEmail(ctx, s.emailService, email.Address{
|
err := SendEmail(ctx, s.emailService, email.Address{
|
||||||
Name: user.FullName(),
|
Name: user.FullName(),
|
||||||
Email: user.Email,
|
Email: *user.Email,
|
||||||
}, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{
|
}, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{
|
||||||
ApiKeyName: apiKey.Name,
|
ApiKeyName: apiKey.Name,
|
||||||
ExpiresAt: apiKey.ExpiresAt.ToTime(),
|
ExpiresAt: apiKey.ExpiresAt.ToTime(),
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
|||||||
// Internal
|
// Internal
|
||||||
InstanceID: model.AppConfigVariable{Value: ""},
|
InstanceID: model.AppConfigVariable{Value: ""},
|
||||||
// Email
|
// Email
|
||||||
|
RequireUserEmail: model.AppConfigVariable{Value: "true"},
|
||||||
SmtpHost: model.AppConfigVariable{},
|
SmtpHost: model.AppConfigVariable{},
|
||||||
SmtpPort: model.AppConfigVariable{},
|
SmtpPort: model.AppConfigVariable{},
|
||||||
SmtpFrom: model.AppConfigVariable{},
|
SmtpFrom: model.AppConfigVariable{},
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (s *AppImagesService) UpdateImage(file *multipart.FileHeader, imageName str
|
|||||||
|
|
||||||
currentExt, ok := s.extensions[imageName]
|
currentExt, ok := s.extensions[imageName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unknown application image '%s'", imageName)
|
s.extensions[imageName] = fileType
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", imageName, fileType))
|
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", imageName, fileType))
|
||||||
@@ -69,13 +69,39 @@ func (s *AppImagesService) UpdateImage(file *multipart.FileHeader, imageName str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AppImagesService) DeleteImage(imageName string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
ext, ok := s.extensions[imageName]
|
||||||
|
if !ok || ext == "" {
|
||||||
|
return &common.ImageNotFoundError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", imageName+"."+ext)
|
||||||
|
if err := os.Remove(imagePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.extensions, imageName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AppImagesService) IsDefaultProfilePictureSet() bool {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
_, ok := s.extensions["default-profile-picture"]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AppImagesService) getExtension(name string) (string, error) {
|
func (s *AppImagesService) getExtension(name string) (string, error) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
ext, ok := s.extensions[name]
|
ext, ok := s.extensions[name]
|
||||||
if !ok || ext == "" {
|
if !ok || ext == "" {
|
||||||
return "", fmt.Errorf("unknown application image '%s'", name)
|
return "", &common.ImageNotFoundError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.ToLower(ext), nil
|
return strings.ToLower(ext), nil
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
userAgentParser "github.com/mileusna/useragent"
|
userAgentParser "github.com/mileusna/useragent"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
@@ -111,9 +110,13 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Email == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
innerErr = SendEmail(innerCtx, s.emailService, email.Address{
|
innerErr = SendEmail(innerCtx, s.emailService, email.Address{
|
||||||
Name: user.FullName(),
|
Name: user.FullName(),
|
||||||
Email: user.Email,
|
Email: *user.Email,
|
||||||
}, NewLoginTemplate, &NewLoginTemplateData{
|
}, NewLoginTemplate, &NewLoginTemplateData{
|
||||||
IPAddress: ipAddress,
|
IPAddress: ipAddress,
|
||||||
Country: createdAuditLog.Country,
|
Country: createdAuditLog.Country,
|
||||||
@@ -122,7 +125,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
|||||||
DateTime: createdAuditLog.CreatedAt.UTC(),
|
DateTime: createdAuditLog.CreatedAt.UTC(),
|
||||||
})
|
})
|
||||||
if innerErr != nil {
|
if innerErr != nil {
|
||||||
slog.ErrorContext(innerCtx, "Failed to send notification email", slog.Any("error", innerErr), slog.String("address", user.Email))
|
slog.ErrorContext(innerCtx, "Failed to send notification email", slog.Any("error", innerErr), slog.String("address", *user.Email))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -132,14 +135,14 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListAuditLogsForUser retrieves all audit logs for a given user ID
|
// ListAuditLogsForUser retrieves all audit logs for a given user ID
|
||||||
func (s *AuditLogService) ListAuditLogsForUser(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.AuditLog, utils.PaginationResponse, error) {
|
func (s *AuditLogService) ListAuditLogsForUser(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||||
var logs []model.AuditLog
|
var logs []model.AuditLog
|
||||||
query := s.db.
|
query := s.db.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Model(&model.AuditLog{}).
|
Model(&model.AuditLog{}).
|
||||||
Where("user_id = ?", userID)
|
Where("user_id = ?", userID)
|
||||||
|
|
||||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
|
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &logs)
|
||||||
return logs, pagination, err
|
return logs, pagination, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +151,7 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
|
|||||||
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPaginationRequest utils.SortedPaginationRequest, filters dto.AuditLogFilterDto) ([]model.AuditLog, utils.PaginationResponse, error) {
|
func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||||
var logs []model.AuditLog
|
var logs []model.AuditLog
|
||||||
|
|
||||||
query := s.db.
|
query := s.db.
|
||||||
@@ -156,33 +159,36 @@ func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPagination
|
|||||||
Preload("User").
|
Preload("User").
|
||||||
Model(&model.AuditLog{})
|
Model(&model.AuditLog{})
|
||||||
|
|
||||||
if filters.UserID != "" {
|
if clientName, ok := listRequestOptions.Filters["clientName"]; ok {
|
||||||
query = query.Where("user_id = ?", filters.UserID)
|
|
||||||
}
|
|
||||||
if filters.Event != "" {
|
|
||||||
query = query.Where("event = ?", filters.Event)
|
|
||||||
}
|
|
||||||
if filters.ClientName != "" {
|
|
||||||
dialect := s.db.Name()
|
dialect := s.db.Name()
|
||||||
switch dialect {
|
switch dialect {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
query = query.Where("json_extract(data, '$.clientName') = ?", filters.ClientName)
|
query = query.Where("json_extract(data, '$.clientName') IN ?", clientName)
|
||||||
case "postgres":
|
case "postgres":
|
||||||
query = query.Where("data->>'clientName' = ?", filters.ClientName)
|
query = query.Where("data->>'clientName' IN ?", clientName)
|
||||||
default:
|
default:
|
||||||
return nil, utils.PaginationResponse{}, fmt.Errorf("unsupported database dialect: %s", dialect)
|
return nil, utils.PaginationResponse{}, fmt.Errorf("unsupported database dialect: %s", dialect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if filters.Location != "" {
|
|
||||||
switch filters.Location {
|
if locations, ok := listRequestOptions.Filters["location"]; ok {
|
||||||
case "external":
|
mapped := make([]string, 0, len(locations))
|
||||||
query = query.Where("country != 'Internal Network'")
|
for _, v := range locations {
|
||||||
case "internal":
|
if s, ok := v.(string); ok {
|
||||||
query = query.Where("country = 'Internal Network'")
|
switch s {
|
||||||
|
case "internal":
|
||||||
|
mapped = append(mapped, "Internal Network")
|
||||||
|
case "external":
|
||||||
|
mapped = append(mapped, "External Network")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mapped) > 0 {
|
||||||
|
query = query.Where("country IN ?", mapped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
|
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &logs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, pagination, err
|
return nil, pagination, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
||||||
},
|
},
|
||||||
Username: "tim",
|
Username: "tim",
|
||||||
Email: "tim.cook@test.com",
|
Email: utils.Ptr("tim.cook@test.com"),
|
||||||
FirstName: "Tim",
|
FirstName: "Tim",
|
||||||
LastName: "Cook",
|
LastName: "Cook",
|
||||||
DisplayName: "Tim Cook",
|
DisplayName: "Tim Cook",
|
||||||
@@ -90,7 +90,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
|
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
|
||||||
},
|
},
|
||||||
Username: "craig",
|
Username: "craig",
|
||||||
Email: "craig.federighi@test.com",
|
Email: utils.Ptr("craig.federighi@test.com"),
|
||||||
FirstName: "Craig",
|
FirstName: "Craig",
|
||||||
LastName: "Federighi",
|
LastName: "Federighi",
|
||||||
DisplayName: "Craig Federighi",
|
DisplayName: "Craig Federighi",
|
||||||
|
|||||||
@@ -62,9 +62,13 @@ func (srv *EmailService) SendTestEmail(ctx context.Context, recipientUserId stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Email == nil {
|
||||||
|
return &common.UserEmailNotSetError{}
|
||||||
|
}
|
||||||
|
|
||||||
return SendEmail(ctx, srv,
|
return SendEmail(ctx, srv,
|
||||||
email.Address{
|
email.Address{
|
||||||
Email: user.Email,
|
Email: *user.Email,
|
||||||
Name: user.FullName(),
|
Name: user.FullName(),
|
||||||
}, TestTemplate, nil)
|
}, TestTemplate, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,35 +13,19 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oschwald/maxminddb-golang/v2"
|
"github.com/oschwald/maxminddb-golang/v2"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GeoLiteService struct {
|
type GeoLiteService struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
disableUpdater bool
|
disableUpdater bool
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
localIPv6Ranges []*net.IPNet
|
|
||||||
}
|
|
||||||
|
|
||||||
var localhostIPNets = []*net.IPNet{
|
|
||||||
{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
|
|
||||||
{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, // ::1/128
|
|
||||||
}
|
|
||||||
|
|
||||||
var privateLanIPNets = []*net.IPNet{
|
|
||||||
{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 10.0.0.0/8
|
|
||||||
{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // 172.16.0.0/12
|
|
||||||
{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // 192.168.0.0/16
|
|
||||||
}
|
|
||||||
|
|
||||||
var tailscaleIPNets = []*net.IPNet{
|
|
||||||
{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}, // 100.64.0.0/10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGeoLiteService initializes a new GeoLiteService instance and starts a goroutine to update the GeoLite2 City database.
|
// NewGeoLiteService initializes a new GeoLiteService instance and starts a goroutine to update the GeoLite2 City database.
|
||||||
@@ -56,67 +40,9 @@ func NewGeoLiteService(httpClient *http.Client) *GeoLiteService {
|
|||||||
service.disableUpdater = true
|
service.disableUpdater = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize IPv6 local ranges
|
|
||||||
err := service.initializeIPv6LocalRanges()
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("Failed to initialize IPv6 local ranges", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// initializeIPv6LocalRanges parses the LOCAL_IPV6_RANGES environment variable
|
|
||||||
func (s *GeoLiteService) initializeIPv6LocalRanges() error {
|
|
||||||
rangesEnv := common.EnvConfig.LocalIPv6Ranges
|
|
||||||
if rangesEnv == "" {
|
|
||||||
return nil // No local IPv6 ranges configured
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges := strings.Split(rangesEnv, ",")
|
|
||||||
localRanges := make([]*net.IPNet, 0, len(ranges))
|
|
||||||
|
|
||||||
for _, rangeStr := range ranges {
|
|
||||||
rangeStr = strings.TrimSpace(rangeStr)
|
|
||||||
if rangeStr == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ipNet, err := net.ParseCIDR(rangeStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid IPv6 range '%s': %w", rangeStr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure it's an IPv6 range
|
|
||||||
if ipNet.IP.To4() != nil {
|
|
||||||
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
localRanges = append(localRanges, ipNet)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.localIPv6Ranges = localRanges
|
|
||||||
|
|
||||||
if len(localRanges) > 0 {
|
|
||||||
slog.Info("Initialized IPv6 local ranges", slog.Int("count", len(localRanges)))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isLocalIPv6 checks if the given IPv6 address is within any of the configured local ranges
|
|
||||||
func (s *GeoLiteService) isLocalIPv6(ip net.IP) bool {
|
|
||||||
if ip.To4() != nil {
|
|
||||||
return false // Not an IPv6 address
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, localRange := range s.localIPv6Ranges {
|
|
||||||
if localRange.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GeoLiteService) DisableUpdater() bool {
|
func (s *GeoLiteService) DisableUpdater() bool {
|
||||||
return s.disableUpdater
|
return s.disableUpdater
|
||||||
}
|
}
|
||||||
@@ -129,26 +55,17 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
|
|||||||
|
|
||||||
// Check the IP address against known private IP ranges
|
// Check the IP address against known private IP ranges
|
||||||
if ip := net.ParseIP(ipAddress); ip != nil {
|
if ip := net.ParseIP(ipAddress); ip != nil {
|
||||||
// Check IPv6 local ranges first
|
if utils.IsLocalIPv6(ip) {
|
||||||
if s.isLocalIPv6(ip) {
|
|
||||||
return "Internal Network", "LAN", nil
|
return "Internal Network", "LAN", nil
|
||||||
}
|
}
|
||||||
|
if utils.IsTailscaleIP(ip) {
|
||||||
// Check existing IPv4 ranges
|
return "Internal Network", "Tailscale", nil
|
||||||
for _, ipNet := range tailscaleIPNets {
|
|
||||||
if ipNet.Contains(ip) {
|
|
||||||
return "Internal Network", "Tailscale", nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, ipNet := range privateLanIPNets {
|
if utils.IsPrivateIP(ip) {
|
||||||
if ipNet.Contains(ip) {
|
return "Internal Network", "LAN", nil
|
||||||
return "Internal Network", "LAN", nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, ipNet := range localhostIPNets {
|
if utils.IsLocalhostIP(ip) {
|
||||||
if ipNet.Contains(ip) {
|
return "Internal Network", "localhost", nil
|
||||||
return "Internal Network", "localhost", nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeoLiteService_IPv6LocalRanges(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
localRanges string
|
|
||||||
testIP string
|
|
||||||
expectedCountry string
|
|
||||||
expectedCity string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "IPv6 in local range",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56,2001:0db8:abcd:001::/56",
|
|
||||||
testIP: "2001:0db8:abcd:000::1",
|
|
||||||
expectedCountry: "Internal Network",
|
|
||||||
expectedCity: "LAN",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv6 not in local range",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "2001:0db8:ffff:000::1",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Multiple ranges - second range match",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56,2001:0db8:abcd:001::/56",
|
|
||||||
testIP: "2001:0db8:abcd:001::1",
|
|
||||||
expectedCountry: "Internal Network",
|
|
||||||
expectedCity: "LAN",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty local ranges",
|
|
||||||
localRanges: "",
|
|
||||||
testIP: "2001:0db8:abcd:000::1",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 private address still works",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "192.168.1.1",
|
|
||||||
expectedCountry: "Internal Network",
|
|
||||||
expectedCity: "LAN",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv6 loopback",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "::1",
|
|
||||||
expectedCountry: "Internal Network",
|
|
||||||
expectedCity: "localhost",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
originalConfig := common.EnvConfig.LocalIPv6Ranges
|
|
||||||
common.EnvConfig.LocalIPv6Ranges = tt.localRanges
|
|
||||||
defer func() {
|
|
||||||
common.EnvConfig.LocalIPv6Ranges = originalConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
service := NewGeoLiteService(&http.Client{})
|
|
||||||
|
|
||||||
country, city, err := service.GetLocationByIP(tt.testIP)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil && country != "Internal Network" {
|
|
||||||
t.Errorf("Expected error or internal network classification for external IP")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expectedCountry, country)
|
|
||||||
assert.Equal(t, tt.expectedCity, city)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGeoLiteService_isLocalIPv6(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
localRanges string
|
|
||||||
testIP string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid IPv6 in range",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "2001:0db8:abcd:000::1",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid IPv6 not in range",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "2001:0db8:ffff:000::1",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 address should return false",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "192.168.1.1",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No ranges configured",
|
|
||||||
localRanges: "",
|
|
||||||
testIP: "2001:0db8:abcd:000::1",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Edge of range",
|
|
||||||
localRanges: "2001:0db8:abcd:000::/56",
|
|
||||||
testIP: "2001:0db8:abcd:00ff:ffff:ffff:ffff:ffff",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
originalConfig := common.EnvConfig.LocalIPv6Ranges
|
|
||||||
common.EnvConfig.LocalIPv6Ranges = tt.localRanges
|
|
||||||
defer func() {
|
|
||||||
common.EnvConfig.LocalIPv6Ranges = originalConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
service := NewGeoLiteService(&http.Client{})
|
|
||||||
ip := net.ParseIP(tt.testIP)
|
|
||||||
if ip == nil {
|
|
||||||
t.Fatalf("Invalid test IP: %s", tt.testIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := service.isLocalIPv6(ip)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGeoLiteService_initializeIPv6LocalRanges(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
envValue string
|
|
||||||
expectError bool
|
|
||||||
expectCount int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid IPv6 ranges",
|
|
||||||
envValue: "2001:0db8:abcd:000::/56,2001:0db8:abcd:001::/56",
|
|
||||||
expectError: false,
|
|
||||||
expectCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty environment variable",
|
|
||||||
envValue: "",
|
|
||||||
expectError: false,
|
|
||||||
expectCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid CIDR notation",
|
|
||||||
envValue: "2001:0db8:abcd:000::/999",
|
|
||||||
expectError: true,
|
|
||||||
expectCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 range in IPv6 env var",
|
|
||||||
envValue: "192.168.1.0/24",
|
|
||||||
expectError: true,
|
|
||||||
expectCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Mixed valid and invalid ranges",
|
|
||||||
envValue: "2001:0db8:abcd:000::/56,invalid-range",
|
|
||||||
expectError: true,
|
|
||||||
expectCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Whitespace handling",
|
|
||||||
envValue: " 2001:0db8:abcd:000::/56 , 2001:0db8:abcd:001::/56 ",
|
|
||||||
expectError: false,
|
|
||||||
expectCount: 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
originalConfig := common.EnvConfig.LocalIPv6Ranges
|
|
||||||
common.EnvConfig.LocalIPv6Ranges = tt.envValue
|
|
||||||
defer func() {
|
|
||||||
common.EnvConfig.LocalIPv6Ranges = originalConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
service := &GeoLiteService{
|
|
||||||
httpClient: &http.Client{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := service.initializeIPv6LocalRanges()
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, service.localIPv6Ranges, tt.expectCount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -342,7 +343,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "user123",
|
ID: "user123",
|
||||||
},
|
},
|
||||||
Email: "user@example.com",
|
Email: utils.Ptr("user@example.com"),
|
||||||
IsAdmin: false,
|
IsAdmin: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +386,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "admin123",
|
ID: "admin123",
|
||||||
},
|
},
|
||||||
Email: "admin@example.com",
|
Email: utils.Ptr("admin@example.com"),
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +465,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "eddsauser123",
|
ID: "eddsauser123",
|
||||||
},
|
},
|
||||||
Email: "eddsauser@example.com",
|
Email: utils.Ptr("eddsauser@example.com"),
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +522,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "ecdsauser123",
|
ID: "ecdsauser123",
|
||||||
},
|
},
|
||||||
Email: "ecdsauser@example.com",
|
Email: utils.Ptr("ecdsauser@example.com"),
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +579,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "rsauser123",
|
ID: "rsauser123",
|
||||||
},
|
},
|
||||||
Email: "rsauser@example.com",
|
Email: utils.Ptr("rsauser@example.com"),
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,7 +966,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "user123",
|
ID: "user123",
|
||||||
},
|
},
|
||||||
Email: "user@example.com",
|
Email: utils.Ptr("user@example.com"),
|
||||||
}
|
}
|
||||||
const clientID = "test-client-123"
|
const clientID = "test-client-123"
|
||||||
|
|
||||||
@@ -1092,7 +1093,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "eddsauser789",
|
ID: "eddsauser789",
|
||||||
},
|
},
|
||||||
Email: "eddsaoauth@example.com",
|
Email: utils.Ptr("eddsaoauth@example.com"),
|
||||||
}
|
}
|
||||||
const clientID = "eddsa-oauth-client"
|
const clientID = "eddsa-oauth-client"
|
||||||
|
|
||||||
@@ -1149,7 +1150,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "ecdsauser789",
|
ID: "ecdsauser789",
|
||||||
},
|
},
|
||||||
Email: "ecdsaoauth@example.com",
|
Email: utils.Ptr("ecdsaoauth@example.com"),
|
||||||
}
|
}
|
||||||
const clientID = "ecdsa-oauth-client"
|
const clientID = "ecdsa-oauth-client"
|
||||||
|
|
||||||
@@ -1206,7 +1207,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "rsauser789",
|
ID: "rsauser789",
|
||||||
},
|
},
|
||||||
Email: "rsaoauth@example.com",
|
Email: utils.Ptr("rsaoauth@example.com"),
|
||||||
}
|
}
|
||||||
const clientID = "rsa-oauth-client"
|
const clientID = "rsa-oauth-client"
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
@@ -348,13 +349,18 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
|
|
||||||
newUser := dto.UserCreateDto{
|
newUser := dto.UserCreateDto{
|
||||||
Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value),
|
Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value),
|
||||||
Email: value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value),
|
Email: utils.PtrOrNil(value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value)),
|
||||||
FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value),
|
FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value),
|
||||||
LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value),
|
LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value),
|
||||||
DisplayName: value.GetAttributeValue(dbConfig.LdapAttributeUserDisplayName.Value),
|
DisplayName: value.GetAttributeValue(dbConfig.LdapAttributeUserDisplayName.Value),
|
||||||
IsAdmin: isAdmin,
|
IsAdmin: isAdmin,
|
||||||
LdapID: ldapId,
|
LdapID: ldapId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newUser.DisplayName == "" {
|
||||||
|
newUser.DisplayName = strings.TrimSpace(newUser.FirstName + " " + newUser.LastName)
|
||||||
|
}
|
||||||
|
|
||||||
dto.Normalize(newUser)
|
dto.Normalize(newUser)
|
||||||
|
|
||||||
err = newUser.Validate()
|
err = newUser.Validate()
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -66,6 +71,7 @@ func NewOidcService(
|
|||||||
auditLogService *AuditLogService,
|
auditLogService *AuditLogService,
|
||||||
customClaimService *CustomClaimService,
|
customClaimService *CustomClaimService,
|
||||||
webAuthnService *WebAuthnService,
|
webAuthnService *WebAuthnService,
|
||||||
|
httpClient *http.Client,
|
||||||
) (s *OidcService, err error) {
|
) (s *OidcService, err error) {
|
||||||
s = &OidcService{
|
s = &OidcService{
|
||||||
db: db,
|
db: db,
|
||||||
@@ -74,6 +80,7 @@ func NewOidcService(
|
|||||||
auditLogService: auditLogService,
|
auditLogService: auditLogService,
|
||||||
customClaimService: customClaimService,
|
customClaimService: customClaimService,
|
||||||
webAuthnService: webAuthnService,
|
webAuthnService: webAuthnService,
|
||||||
|
httpClient: httpClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: we don't pass the HTTP Client with OTel instrumented to this because requests are always made in background and not tied to a specific trace
|
// Note: we don't pass the HTTP Client with OTel instrumented to this because requests are always made in background and not tied to a specific trace
|
||||||
@@ -388,7 +395,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, inpu
|
|||||||
|
|
||||||
// If the client is public or PKCE is enabled, the code verifier must match the code challenge
|
// If the client is public or PKCE is enabled, the code verifier must match the code challenge
|
||||||
if client.IsPublic || client.PkceEnabled {
|
if client.IsPublic || client.PkceEnabled {
|
||||||
if !s.validateCodeVerifier(input.CodeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
if !validateCodeVerifier(input.CodeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
||||||
return CreatedTokens{}, &common.OidcInvalidCodeVerifierError{}
|
return CreatedTokens{}, &common.OidcInvalidCodeVerifierError{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,7 +476,7 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, input dto
|
|||||||
var storedRefreshToken model.OidcRefreshToken
|
var storedRefreshToken model.OidcRefreshToken
|
||||||
err = tx.
|
err = tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("User").
|
Preload("User.UserGroups").
|
||||||
Where(
|
Where(
|
||||||
"token = ? AND expires_at > ? AND user_id = ? AND client_id = ?",
|
"token = ? AND expires_at > ? AND user_id = ? AND client_id = ?",
|
||||||
utils.CreateSha256Hash(rt),
|
utils.CreateSha256Hash(rt),
|
||||||
@@ -686,7 +693,7 @@ func (s *OidcService) getClientInternal(ctx context.Context, clientID string, tx
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) ListClients(ctx context.Context, name string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.OidcClient, utils.PaginationResponse, error) {
|
func (s *OidcService) ListClients(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) ([]model.OidcClient, utils.PaginationResponse, error) {
|
||||||
var clients []model.OidcClient
|
var clients []model.OidcClient
|
||||||
|
|
||||||
query := s.db.
|
query := s.db.
|
||||||
@@ -699,21 +706,26 @@ func (s *OidcService) ListClients(ctx context.Context, name string, sortedPagina
|
|||||||
}
|
}
|
||||||
|
|
||||||
// As allowedUserGroupsCount is not a column, we need to manually sort it
|
// As allowedUserGroupsCount is not a column, we need to manually sort it
|
||||||
if sortedPaginationRequest.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
if listRequestOptions.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
|
||||||
query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
|
query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
|
||||||
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
|
||||||
Group("oidc_clients.id").
|
Group("oidc_clients.id").
|
||||||
Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + sortedPaginationRequest.Sort.Direction)
|
Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + listRequestOptions.Sort.Direction)
|
||||||
|
|
||||||
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &clients)
|
response, err := utils.Paginate(listRequestOptions.Pagination.Page, listRequestOptions.Pagination.Limit, query, &clients)
|
||||||
return clients, response, err
|
return clients, response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
|
response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
|
||||||
return clients, response, err
|
return clients, response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) {
|
func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) {
|
||||||
|
tx := s.db.Begin()
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
client := model.OidcClient{
|
client := model.OidcClient{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
@@ -722,7 +734,7 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
|
|||||||
}
|
}
|
||||||
updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)
|
updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)
|
||||||
|
|
||||||
err := s.db.
|
err := tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Create(&client).
|
Create(&client).
|
||||||
Error
|
Error
|
||||||
@@ -733,33 +745,18 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
|
|||||||
return model.OidcClient{}, err
|
return model.OidcClient{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
if input.LogoURL != nil {
|
||||||
}
|
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL, true)
|
||||||
|
if err != nil {
|
||||||
func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientUpdateDto) (model.OidcClient, error) {
|
return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
|
||||||
tx := s.db.Begin()
|
}
|
||||||
defer func() {
|
|
||||||
tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var client model.OidcClient
|
|
||||||
err := tx.
|
|
||||||
WithContext(ctx).
|
|
||||||
Preload("CreatedBy").
|
|
||||||
First(&client, "id = ?", clientID).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return model.OidcClient{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOIDCClientModelFromDto(&client, &input)
|
if input.DarkLogoURL != nil {
|
||||||
|
err = s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.DarkLogoURL, false)
|
||||||
err = tx.
|
if err != nil {
|
||||||
WithContext(ctx).
|
return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
|
||||||
Save(&client).
|
}
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return model.OidcClient{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit().Error
|
err = tx.Commit().Error
|
||||||
@@ -770,6 +767,43 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientUpdateDto) (model.OidcClient, error) {
|
||||||
|
tx := s.db.Begin()
|
||||||
|
defer func() { tx.Rollback() }()
|
||||||
|
|
||||||
|
var client model.OidcClient
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Preload("CreatedBy").
|
||||||
|
First(&client, "id = ?", clientID).Error; err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOIDCClientModelFromDto(&client, &input)
|
||||||
|
|
||||||
|
if err := tx.WithContext(ctx).Save(&client).Error; err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.LogoURL != nil {
|
||||||
|
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.LogoURL, true)
|
||||||
|
if err != nil {
|
||||||
|
return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.DarkLogoURL != nil {
|
||||||
|
err := s.downloadAndSaveLogoFromURL(ctx, tx, client.ID, *input.DarkLogoURL, false)
|
||||||
|
if err != nil {
|
||||||
|
return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
|
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
|
||||||
// Base fields
|
// Base fields
|
||||||
client.Name = input.Name
|
client.Name = input.Name
|
||||||
@@ -850,7 +884,7 @@ func (s *OidcService) CreateClientSecret(ctx context.Context, clientID string) (
|
|||||||
return clientSecret, nil
|
return clientSecret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (string, string, error) {
|
func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light bool) (string, string, error) {
|
||||||
var client model.OidcClient
|
var client model.OidcClient
|
||||||
err := s.db.
|
err := s.db.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -860,64 +894,52 @@ func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (strin
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.ImageType == nil {
|
var imagePath, mimeType string
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !light && client.DarkImageType != nil:
|
||||||
|
// Dark logo if requested and exists
|
||||||
|
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + *client.DarkImageType
|
||||||
|
mimeType = utils.GetImageMimeType(*client.DarkImageType)
|
||||||
|
|
||||||
|
case client.ImageType != nil:
|
||||||
|
// Light logo if requested or no dark logo is available
|
||||||
|
imagePath = common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType
|
||||||
|
mimeType = utils.GetImageMimeType(*client.ImageType)
|
||||||
|
|
||||||
|
default:
|
||||||
return "", "", errors.New("image not found")
|
return "", "", errors.New("image not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "." + *client.ImageType
|
|
||||||
mimeType := utils.GetImageMimeType(*client.ImageType)
|
|
||||||
|
|
||||||
return imagePath, mimeType, nil
|
return imagePath, mimeType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader) error {
|
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader, light bool) error {
|
||||||
fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
|
fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
|
||||||
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
|
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
|
||||||
return &common.FileTypeNotSupportedError{}
|
return &common.FileTypeNotSupportedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + "." + fileType
|
var darkSuffix string
|
||||||
|
if !light {
|
||||||
|
darkSuffix = "-dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + clientID + darkSuffix + "." + fileType
|
||||||
err := utils.SaveFile(file, imagePath)
|
err := utils.SaveFile(file, imagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
|
||||||
|
err = s.updateClientLogoType(ctx, tx, clientID, fileType, light)
|
||||||
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
}()
|
|
||||||
|
|
||||||
var client model.OidcClient
|
|
||||||
err = tx.
|
|
||||||
WithContext(ctx).
|
|
||||||
First(&client, "id = ?", clientID).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.ImageType != nil && fileType != *client.ImageType {
|
return tx.Commit().Error
|
||||||
oldImagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, client.ID, *client.ImageType)
|
|
||||||
if err := os.Remove(oldImagePath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.ImageType = &fileType
|
|
||||||
err = tx.
|
|
||||||
WithContext(ctx).
|
|
||||||
Save(&client).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit().Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) error {
|
func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) error {
|
||||||
@@ -941,6 +963,7 @@ func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) err
|
|||||||
|
|
||||||
oldImageType := *client.ImageType
|
oldImageType := *client.ImageType
|
||||||
client.ImageType = nil
|
client.ImageType = nil
|
||||||
|
|
||||||
err = tx.
|
err = tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Save(&client).
|
Save(&client).
|
||||||
@@ -962,6 +985,49 @@ func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OidcService) DeleteClientDarkLogo(ctx context.Context, clientID string) error {
|
||||||
|
tx := s.db.Begin()
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var client model.OidcClient
|
||||||
|
err := tx.
|
||||||
|
WithContext(ctx).
|
||||||
|
First(&client, "id = ?", clientID).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.DarkImageType == nil {
|
||||||
|
return errors.New("image not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldImageType := *client.DarkImageType
|
||||||
|
client.DarkImageType = nil
|
||||||
|
|
||||||
|
err = tx.
|
||||||
|
WithContext(ctx).
|
||||||
|
Save(&client).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePath := common.EnvConfig.UploadPath + "/oidc-client-images/" + client.ID + "-dark." + oldImageType
|
||||||
|
if err := os.Remove(imagePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit().Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) {
|
func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) {
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -1090,13 +1156,20 @@ func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID stri
|
|||||||
return randomString, nil
|
return randomString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
func validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
||||||
if codeVerifier == "" || codeChallenge == "" {
|
if codeVerifier == "" || codeChallenge == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !codeChallengeMethodSha256 {
|
if !codeChallengeMethodSha256 {
|
||||||
return codeVerifier == codeChallenge
|
return subtle.ConstantTimeCompare([]byte(codeVerifier), []byte(codeChallenge)) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 URL decode the challenge
|
||||||
|
// If it's not valid base64url, fail the operation
|
||||||
|
codeChallengeBytes, err := base64.RawURLEncoding.DecodeString(codeChallenge)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute SHA-256 hash of the codeVerifier
|
// Compute SHA-256 hash of the codeVerifier
|
||||||
@@ -1104,10 +1177,7 @@ func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, c
|
|||||||
h.Write([]byte(codeVerifier))
|
h.Write([]byte(codeVerifier))
|
||||||
codeVerifierHash := h.Sum(nil)
|
codeVerifierHash := h.Sum(nil)
|
||||||
|
|
||||||
// Base64 URL encode the verifier hash
|
return subtle.ConstantTimeCompare(codeVerifierHash, codeChallengeBytes) == 1
|
||||||
encodedVerifierHash := base64.RawURLEncoding.EncodeToString(codeVerifierHash)
|
|
||||||
|
|
||||||
return encodedVerifierHash == codeChallenge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) getCallbackURL(client *model.OidcClient, inputCallbackURL string, tx *gorm.DB, ctx context.Context) (callbackURL string, err error) {
|
func (s *OidcService) getCallbackURL(client *model.OidcClient, inputCallbackURL string, tx *gorm.DB, ctx context.Context) (callbackURL string, err error) {
|
||||||
@@ -1331,9 +1401,10 @@ func (s *OidcService) GetDeviceCodeInfo(ctx context.Context, userCode string, us
|
|||||||
|
|
||||||
return &dto.DeviceCodeInfoDto{
|
return &dto.DeviceCodeInfoDto{
|
||||||
Client: dto.OidcClientMetaDataDto{
|
Client: dto.OidcClientMetaDataDto{
|
||||||
ID: deviceAuth.Client.ID,
|
ID: deviceAuth.Client.ID,
|
||||||
Name: deviceAuth.Client.Name,
|
Name: deviceAuth.Client.Name,
|
||||||
HasLogo: deviceAuth.Client.HasLogo,
|
HasLogo: deviceAuth.Client.HasLogo(),
|
||||||
|
HasDarkLogo: deviceAuth.Client.HasDarkLogo(),
|
||||||
},
|
},
|
||||||
Scope: deviceAuth.Scope,
|
Scope: deviceAuth.Scope,
|
||||||
AuthorizationRequired: !hasAuthorizedClient,
|
AuthorizationRequired: !hasAuthorizedClient,
|
||||||
@@ -1357,7 +1428,7 @@ func (s *OidcService) GetAllowedGroupsCountOfClient(ctx context.Context, id stri
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {
|
func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {
|
||||||
|
|
||||||
query := s.db.
|
query := s.db.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -1366,7 +1437,7 @@ func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string,
|
|||||||
Where("user_id = ?", userID)
|
Where("user_id = ?", userID)
|
||||||
|
|
||||||
var authorizedClients []model.UserAuthorizedOidcClient
|
var authorizedClients []model.UserAuthorizedOidcClient
|
||||||
response, err := utils.PaginateAndSort(sortedPaginationRequest, query, &authorizedClients)
|
response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &authorizedClients)
|
||||||
|
|
||||||
return authorizedClients, response, err
|
return authorizedClients, response, err
|
||||||
}
|
}
|
||||||
@@ -1399,7 +1470,7 @@ func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
|
func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -1446,13 +1517,13 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
|||||||
|
|
||||||
// Handle custom sorting for lastUsedAt column
|
// Handle custom sorting for lastUsedAt column
|
||||||
var response utils.PaginationResponse
|
var response utils.PaginationResponse
|
||||||
if sortedPaginationRequest.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
if listRequestOptions.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
|
||||||
query = query.
|
query = query.
|
||||||
Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
|
Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
|
||||||
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction + " NULLS LAST")
|
Order("user_authorized_oidc_clients.last_used_at " + listRequestOptions.Sort.Direction + " NULLS LAST")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
|
response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, utils.PaginationResponse{}, err
|
return nil, utils.PaginationResponse{}, err
|
||||||
}
|
}
|
||||||
@@ -1465,10 +1536,11 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
|||||||
}
|
}
|
||||||
dtos[i] = dto.AccessibleOidcClientDto{
|
dtos[i] = dto.AccessibleOidcClientDto{
|
||||||
OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
|
OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
|
||||||
ID: client.ID,
|
ID: client.ID,
|
||||||
Name: client.Name,
|
Name: client.Name,
|
||||||
LaunchURL: client.LaunchURL,
|
LaunchURL: client.LaunchURL,
|
||||||
HasLogo: client.HasLogo,
|
HasLogo: client.HasLogo(),
|
||||||
|
HasDarkLogo: client.HasDarkLogo(),
|
||||||
},
|
},
|
||||||
LastUsedAt: lastUsedAt,
|
LastUsedAt: lastUsedAt,
|
||||||
}
|
}
|
||||||
@@ -1889,3 +1961,113 @@ func (s *OidcService) IsClientAccessibleToUser(ctx context.Context, clientID str
|
|||||||
|
|
||||||
return s.IsUserGroupAllowedToAuthorize(user, client), nil
|
return s.IsUserGroupAllowedToAuthorize(user, client), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, tx *gorm.DB, clientID string, raw string, light bool) error {
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
r := net.Resolver{}
|
||||||
|
ips, err := r.LookupIPAddr(ctx, u.Hostname())
|
||||||
|
if err != nil || len(ips) == 0 {
|
||||||
|
return fmt.Errorf("cannot resolve hostname")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevents SSRF by allowing only public IPs
|
||||||
|
for _, addr := range ips {
|
||||||
|
if utils.IsPrivateIP(addr.IP) {
|
||||||
|
return fmt.Errorf("private IP addresses are not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, raw, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "pocket-id/oidc-logo-fetcher")
|
||||||
|
req.Header.Set("Accept", "image/*")
|
||||||
|
|
||||||
|
resp, err := s.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to fetch logo: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLogoSize int64 = 2 * 1024 * 1024 // 2MB
|
||||||
|
if resp.ContentLength > maxLogoSize {
|
||||||
|
return fmt.Errorf("logo is too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer extension in path if supported
|
||||||
|
ext := utils.GetFileExtension(u.Path)
|
||||||
|
if ext == "" || utils.GetImageMimeType(ext) == "" {
|
||||||
|
// Otherwise, try to detect from content type
|
||||||
|
ext = utils.GetImageExtensionFromMimeType(resp.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext == "" {
|
||||||
|
return &common.FileTypeNotSupportedError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
folderPath := filepath.Join(common.EnvConfig.UploadPath, "oidc-client-images")
|
||||||
|
err = os.MkdirAll(folderPath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var darkSuffix string
|
||||||
|
if !light {
|
||||||
|
darkSuffix = "-dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePath := filepath.Join(folderPath, clientID+darkSuffix+"."+ext)
|
||||||
|
err = utils.SaveFileStream(io.LimitReader(resp.Body, maxLogoSize+1), imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.updateClientLogoType(ctx, tx, clientID, ext, light); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OidcService) updateClientLogoType(ctx context.Context, tx *gorm.DB, clientID, ext string, light bool) error {
|
||||||
|
uploadsDir := common.EnvConfig.UploadPath + "/oidc-client-images"
|
||||||
|
|
||||||
|
var darkSuffix string
|
||||||
|
if !light {
|
||||||
|
darkSuffix = "-dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
var client model.OidcClient
|
||||||
|
if err := tx.WithContext(ctx).First(&client, "id = ?", clientID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if client.ImageType != nil && *client.ImageType != ext {
|
||||||
|
old := fmt.Sprintf("%s/%s%s.%s", uploadsDir, client.ID, darkSuffix, *client.ImageType)
|
||||||
|
_ = os.Remove(old)
|
||||||
|
}
|
||||||
|
|
||||||
|
var column string
|
||||||
|
if light {
|
||||||
|
column = "image_type"
|
||||||
|
} else {
|
||||||
|
column = "dark_image_type"
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.WithContext(ctx).
|
||||||
|
Model(&model.OidcClient{}).
|
||||||
|
Where("id = ?", clientID).
|
||||||
|
Update(column, ext).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -510,3 +512,28 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateCodeVerifier_Plain(t *testing.T) {
|
||||||
|
require.False(t, validateCodeVerifier("", "", false))
|
||||||
|
require.False(t, validateCodeVerifier("", "", true))
|
||||||
|
|
||||||
|
t.Run("plain", func(t *testing.T) {
|
||||||
|
require.False(t, validateCodeVerifier("", "challenge", false))
|
||||||
|
require.False(t, validateCodeVerifier("verifier", "", false))
|
||||||
|
require.True(t, validateCodeVerifier("plainVerifier", "plainVerifier", false))
|
||||||
|
require.False(t, validateCodeVerifier("plainVerifier", "otherVerifier", false))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SHA 256", func(t *testing.T) {
|
||||||
|
codeVerifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||||
|
hash := sha256.Sum256([]byte(codeVerifier))
|
||||||
|
codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
require.True(t, validateCodeVerifier(codeVerifier, codeChallenge, true))
|
||||||
|
require.False(t, validateCodeVerifier("wrongVerifier", codeChallenge, true))
|
||||||
|
require.False(t, validateCodeVerifier(codeVerifier, "!", true))
|
||||||
|
|
||||||
|
// Invalid base64
|
||||||
|
require.False(t, validateCodeVerifier("NOT!VALID", codeChallenge, true))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService) *UserG
|
|||||||
return &UserGroupService{db: db, appConfigService: appConfigService}
|
return &UserGroupService{db: db, appConfigService: appConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
func (s *UserGroupService) List(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
||||||
query := s.db.
|
query := s.db.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("CustomClaims").
|
Preload("CustomClaims").
|
||||||
@@ -32,17 +32,14 @@ func (s *UserGroupService) List(ctx context.Context, name string, sortedPaginati
|
|||||||
}
|
}
|
||||||
|
|
||||||
// As userCount is not a column we need to manually sort it
|
// As userCount is not a column we need to manually sort it
|
||||||
if sortedPaginationRequest.Sort.Column == "userCount" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
|
if listRequestOptions.Sort.Column == "userCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
|
||||||
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
|
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
|
||||||
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
|
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
|
||||||
Group("user_groups.id").
|
Group("user_groups.id").
|
||||||
Order("COUNT(user_groups_users.user_id) " + sortedPaginationRequest.Sort.Direction)
|
Order("COUNT(user_groups_users.user_id) " + listRequestOptions.Sort.Direction)
|
||||||
|
|
||||||
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &groups)
|
|
||||||
return groups, response, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &groups)
|
response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &groups)
|
||||||
return groups, response, err
|
return groups, response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -33,9 +34,10 @@ type UserService struct {
|
|||||||
emailService *EmailService
|
emailService *EmailService
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
customClaimService *CustomClaimService
|
customClaimService *CustomClaimService
|
||||||
|
appImagesService *AppImagesService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService) *UserService {
|
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService) *UserService {
|
||||||
return &UserService{
|
return &UserService{
|
||||||
db: db,
|
db: db,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
@@ -43,10 +45,11 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
|
|||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
customClaimService: customClaimService,
|
customClaimService: customClaimService,
|
||||||
|
appImagesService: appImagesService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, listRequestOptions utils.ListRequestOptions) ([]model.User, utils.PaginationResponse, error) {
|
||||||
var users []model.User
|
var users []model.User
|
||||||
query := s.db.WithContext(ctx).
|
query := s.db.WithContext(ctx).
|
||||||
Model(&model.User{}).
|
Model(&model.User{}).
|
||||||
@@ -60,7 +63,7 @@ func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPa
|
|||||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users)
|
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &users)
|
||||||
|
|
||||||
return users, pagination, err
|
return users, pagination, err
|
||||||
}
|
}
|
||||||
@@ -87,39 +90,42 @@ func (s *UserService) GetProfilePicture(ctx context.Context, userID string) (io.
|
|||||||
return nil, 0, &common.InvalidUUIDError{}
|
return nil, 0, &common.InvalidUUIDError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check for a custom uploaded profile picture (userID.png)
|
|
||||||
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png"
|
|
||||||
file, err := os.Open(profilePicturePath)
|
|
||||||
if err == nil {
|
|
||||||
// Get the file size
|
|
||||||
fileInfo, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return file, fileInfo.Size(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no custom picture exists, get the user's data for creating initials
|
|
||||||
user, err := s.GetUser(ctx, userID)
|
user, err := s.GetUser(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have a cached default picture for these initials
|
profilePicturePath := filepath.Join(common.EnvConfig.UploadPath, "profile-pictures", userID+".png")
|
||||||
defaultProfilePicturesDir := common.EnvConfig.UploadPath + "/profile-pictures/defaults/"
|
|
||||||
defaultPicturePath := defaultProfilePicturesDir + user.Initials() + ".png"
|
// Try custom profile picture
|
||||||
file, err = os.Open(defaultPicturePath)
|
if file, size, err := utils.OpenFileWithSize(profilePicturePath); err == nil {
|
||||||
if err == nil {
|
return file, size, nil
|
||||||
fileInfo, err := file.Stat()
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
if err != nil {
|
return nil, 0, err
|
||||||
file.Close()
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return file, fileInfo.Size(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no cached default picture exists, create one and save it for future use
|
// Try default global profile picture
|
||||||
|
if s.appImagesService.IsDefaultProfilePictureSet() {
|
||||||
|
path, _, err := s.appImagesService.GetImage("default-profile-picture")
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if file, size, err := utils.OpenFileWithSize(path); err == nil {
|
||||||
|
return file, size, nil
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try cached default for initials
|
||||||
|
defaultProfilePicturesDir := filepath.Join(common.EnvConfig.UploadPath, "profile-pictures", "defaults")
|
||||||
|
defaultPicturePath := filepath.Join(defaultProfilePicturesDir, user.Initials()+".png")
|
||||||
|
|
||||||
|
if file, size, err := utils.OpenFileWithSize(defaultPicturePath); err == nil {
|
||||||
|
return file, size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return generated default with initials
|
||||||
defaultPicture, err := profilepicture.CreateDefaultProfilePicture(user.Initials())
|
defaultPicture, err := profilepicture.CreateDefaultProfilePicture(user.Initials())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -128,19 +134,16 @@ func (s *UserService) GetProfilePicture(ctx context.Context, userID string) (io.
|
|||||||
// Save the default picture for future use (in a goroutine to avoid blocking)
|
// Save the default picture for future use (in a goroutine to avoid blocking)
|
||||||
defaultPictureBytes := defaultPicture.Bytes()
|
defaultPictureBytes := defaultPicture.Bytes()
|
||||||
go func() {
|
go func() {
|
||||||
// Ensure the directory exists
|
if err := os.MkdirAll(defaultProfilePicturesDir, os.ModePerm); err != nil {
|
||||||
errInternal := os.MkdirAll(defaultProfilePicturesDir, os.ModePerm)
|
slog.Error("Failed to create directory for default profile picture", slog.Any("error", err))
|
||||||
if errInternal != nil {
|
|
||||||
slog.Error("Failed to create directory for default profile picture", slog.Any("error", errInternal))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errInternal = utils.SaveFileStream(bytes.NewReader(defaultPictureBytes), defaultPicturePath)
|
if err := utils.SaveFileStream(bytes.NewReader(defaultPictureBytes), defaultPicturePath); err != nil {
|
||||||
if errInternal != nil {
|
slog.Error("Failed to cache default profile picture", slog.String("initials", user.Initials()), slog.Any("error", err))
|
||||||
slog.Error("Failed to cache default profile picture for initials", slog.String("initials", user.Initials()), slog.Any("error", errInternal))
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return io.NopCloser(bytes.NewReader(defaultPictureBytes)), int64(defaultPicture.Len()), nil
|
return io.NopCloser(bytes.NewReader(defaultPictureBytes)), int64(len(defaultPictureBytes)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) GetUserGroups(ctx context.Context, userID string) ([]model.UserGroup, error) {
|
func (s *UserService) GetUserGroups(ctx context.Context, userID string) ([]model.UserGroup, error) {
|
||||||
@@ -244,6 +247,10 @@ func (s *UserService) CreateUser(ctx context.Context, input dto.UserCreateDto) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
||||||
|
if s.appConfigService.GetDbConfig().RequireUserEmail.IsTrue() && input.Email == nil {
|
||||||
|
return model.User{}, &common.UserEmailNotSetError{}
|
||||||
|
}
|
||||||
|
|
||||||
user := model.User{
|
user := model.User{
|
||||||
FirstName: input.FirstName,
|
FirstName: input.FirstName,
|
||||||
LastName: input.LastName,
|
LastName: input.LastName,
|
||||||
@@ -252,6 +259,7 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
|
|||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
IsAdmin: input.IsAdmin,
|
IsAdmin: input.IsAdmin,
|
||||||
Locale: input.Locale,
|
Locale: input.Locale,
|
||||||
|
Disabled: input.Disabled,
|
||||||
}
|
}
|
||||||
if input.LdapID != "" {
|
if input.LdapID != "" {
|
||||||
user.LdapID = &input.LdapID
|
user.LdapID = &input.LdapID
|
||||||
@@ -339,6 +347,10 @@ func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) updateUserInternal(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
func (s *UserService) updateUserInternal(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
||||||
|
if s.appConfigService.GetDbConfig().RequireUserEmail.IsTrue() && updatedUser.Email == nil {
|
||||||
|
return model.User{}, &common.UserEmailNotSetError{}
|
||||||
|
}
|
||||||
|
|
||||||
var user model.User
|
var user model.User
|
||||||
err := tx.
|
err := tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -437,6 +449,10 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Email == nil {
|
||||||
|
return &common.UserEmailNotSetError{}
|
||||||
|
}
|
||||||
|
|
||||||
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
|
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -464,7 +480,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
|||||||
|
|
||||||
errInternal := SendEmail(innerCtx, s.emailService, email.Address{
|
errInternal := SendEmail(innerCtx, s.emailService, email.Address{
|
||||||
Name: user.FullName(),
|
Name: user.FullName(),
|
||||||
Email: user.Email,
|
Email: *user.Email,
|
||||||
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
||||||
Code: oneTimeAccessToken,
|
Code: oneTimeAccessToken,
|
||||||
LoginLink: link,
|
LoginLink: link,
|
||||||
@@ -472,7 +488,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use
|
|||||||
ExpirationString: utils.DurationToString(ttl),
|
ExpirationString: utils.DurationToString(ttl),
|
||||||
})
|
})
|
||||||
if errInternal != nil {
|
if errInternal != nil {
|
||||||
slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email))
|
slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", *user.Email))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -782,11 +798,11 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
|
|||||||
return user, accessToken, nil
|
return user, accessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ListSignupTokens(ctx context.Context, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.SignupToken, utils.PaginationResponse, error) {
|
func (s *UserService) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.SignupToken, utils.PaginationResponse, error) {
|
||||||
var tokens []model.SignupToken
|
var tokens []model.SignupToken
|
||||||
query := s.db.WithContext(ctx).Model(&model.SignupToken{})
|
query := s.db.WithContext(ctx).Model(&model.SignupToken{})
|
||||||
|
|
||||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &tokens)
|
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &tokens)
|
||||||
return tokens, pagination, err
|
return tokens, pagination, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
|||||||
&user,
|
&user,
|
||||||
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
|
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
|
||||||
webauthn.WithExclusions(user.WebAuthnCredentialDescriptors()),
|
webauthn.WithExclusions(user.WebAuthnCredentialDescriptors()),
|
||||||
|
webauthn.WithExtensions(map[string]any{"credProps": true}), // Required for Firefox Android to properly save the key in Google password manager
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)
|
return nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)
|
||||||
@@ -89,6 +90,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
|||||||
sessionToStore := &model.WebauthnSession{
|
sessionToStore := &model.WebauthnSession{
|
||||||
ExpiresAt: datatype.DateTime(session.Expires),
|
ExpiresAt: datatype.DateTime(session.Expires),
|
||||||
Challenge: session.Challenge,
|
Challenge: session.Challenge,
|
||||||
|
CredentialParams: session.CredParams,
|
||||||
UserVerification: string(session.UserVerification),
|
UserVerification: string(session.UserVerification),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,9 +132,10 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
|
|||||||
}
|
}
|
||||||
|
|
||||||
session := webauthn.SessionData{
|
session := webauthn.SessionData{
|
||||||
Challenge: storedSession.Challenge,
|
Challenge: storedSession.Challenge,
|
||||||
Expires: storedSession.ExpiresAt.ToTime(),
|
Expires: storedSession.ExpiresAt.ToTime(),
|
||||||
UserID: []byte(userID),
|
CredParams: storedSession.CredentialParams,
|
||||||
|
UserID: []byte(userID),
|
||||||
}
|
}
|
||||||
|
|
||||||
var user model.User
|
var user model.User
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -57,6 +58,34 @@ func GetImageMimeType(ext string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetImageExtensionFromMimeType(mimeType string) string {
|
||||||
|
// Normalize and strip parameters like `; charset=utf-8`
|
||||||
|
mt := strings.TrimSpace(strings.ToLower(mimeType))
|
||||||
|
if v, _, err := mime.ParseMediaType(mt); err == nil {
|
||||||
|
mt = v
|
||||||
|
}
|
||||||
|
switch mt {
|
||||||
|
case "image/jpeg", "image/jpg":
|
||||||
|
return "jpg"
|
||||||
|
case "image/png":
|
||||||
|
return "png"
|
||||||
|
case "image/svg+xml":
|
||||||
|
return "svg"
|
||||||
|
case "image/x-icon", "image/vnd.microsoft.icon":
|
||||||
|
return "ico"
|
||||||
|
case "image/gif":
|
||||||
|
return "gif"
|
||||||
|
case "image/webp":
|
||||||
|
return "webp"
|
||||||
|
case "image/avif":
|
||||||
|
return "avif"
|
||||||
|
case "image/heic", "image/heif":
|
||||||
|
return "heic"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
|
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
|
||||||
srcFile, err := resources.FS.Open(srcFilePath)
|
srcFile, err := resources.FS.Open(srcFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -210,3 +239,17 @@ func IsWritableDir(dir string) (bool, error) {
|
|||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenFileWithSize opens a file and returns its size
|
||||||
|
func OpenFileWithSize(path string) (io.ReadCloser, int64, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return f, info.Size(), nil
|
||||||
|
}
|
||||||
|
|||||||
87
backend/internal/utils/ip_util.go
Normal file
87
backend/internal/utils/ip_util.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var localIPv6Ranges []*net.IPNet
|
||||||
|
|
||||||
|
var localhostIPNets = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
|
||||||
|
{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, // ::1/128
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateLanIPNets = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 10.0.0.0/8
|
||||||
|
{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // 172.16.0.0/12
|
||||||
|
{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // 192.168.0.0/16
|
||||||
|
}
|
||||||
|
|
||||||
|
var tailscaleIPNets = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}, // 100.64.0.0/10
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsLocalIPv6(ip net.IP) bool {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return listContainsIP(localIPv6Ranges, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsLocalhostIP(ip net.IP) bool {
|
||||||
|
return listContainsIP(localhostIPNets, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPrivateLanIP(ip net.IP) bool {
|
||||||
|
if ip.To4() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return listContainsIP(privateLanIPNets, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsTailscaleIP(ip net.IP) bool {
|
||||||
|
if ip.To4() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return listContainsIP(tailscaleIPNets, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPrivateIP(ip net.IP) bool {
|
||||||
|
return IsLocalhostIP(ip) || IsPrivateLanIP(ip) || IsTailscaleIP(ip) || IsLocalIPv6(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listContainsIP(ipNets []*net.IPNet, ip net.IP) bool {
|
||||||
|
for _, ipNet := range ipNets {
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadLocalIPv6Ranges() {
|
||||||
|
localIPv6Ranges = nil
|
||||||
|
ranges := strings.Split(common.EnvConfig.LocalIPv6Ranges, ",")
|
||||||
|
|
||||||
|
for _, rangeStr := range ranges {
|
||||||
|
rangeStr = strings.TrimSpace(rangeStr)
|
||||||
|
if rangeStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||||
|
if err == nil {
|
||||||
|
localIPv6Ranges = append(localIPv6Ranges, ipNet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
loadLocalIPv6Ranges()
|
||||||
|
}
|
||||||
159
backend/internal/utils/ip_util_test.go
Normal file
159
backend/internal/utils/ip_util_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsLocalhostIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"127.0.0.1", true},
|
||||||
|
{"127.255.255.255", true},
|
||||||
|
{"::1", true},
|
||||||
|
{"192.168.1.1", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if got := IsLocalhostIP(ip); got != tt.expected {
|
||||||
|
t.Errorf("IsLocalhostIP(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivateLanIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"10.0.0.1", true},
|
||||||
|
{"172.16.5.4", true},
|
||||||
|
{"192.168.100.200", true},
|
||||||
|
{"8.8.8.8", false},
|
||||||
|
{"::1", false}, // IPv6 should return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if got := IsPrivateLanIP(ip); got != tt.expected {
|
||||||
|
t.Errorf("IsPrivateLanIP(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsTailscaleIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"100.64.0.1", true},
|
||||||
|
{"100.127.255.254", true},
|
||||||
|
{"8.8.8.8", false},
|
||||||
|
{"::1", false}, // IPv6 should return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if got := IsTailscaleIP(ip); got != tt.expected {
|
||||||
|
t.Errorf("IsTailscaleIP(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsLocalIPv6(t *testing.T) {
|
||||||
|
// Save and restore env config
|
||||||
|
origRanges := common.EnvConfig.LocalIPv6Ranges
|
||||||
|
defer func() { common.EnvConfig.LocalIPv6Ranges = origRanges }()
|
||||||
|
|
||||||
|
common.EnvConfig.LocalIPv6Ranges = "fd00::/8,fc00::/7"
|
||||||
|
localIPv6Ranges = nil // reset
|
||||||
|
loadLocalIPv6Ranges()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"fd00::1", true},
|
||||||
|
{"fc00::abcd", true},
|
||||||
|
{"::1", false}, // loopback handled separately
|
||||||
|
{"192.168.1.1", false}, // IPv4 should return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if got := IsLocalIPv6(ip); got != tt.expected {
|
||||||
|
t.Errorf("IsLocalIPv6(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivateIP(t *testing.T) {
|
||||||
|
// Save and restore env config
|
||||||
|
origRanges := common.EnvConfig.LocalIPv6Ranges
|
||||||
|
defer func() { common.EnvConfig.LocalIPv6Ranges = origRanges }()
|
||||||
|
|
||||||
|
common.EnvConfig.LocalIPv6Ranges = "fd00::/8"
|
||||||
|
localIPv6Ranges = nil // reset
|
||||||
|
loadLocalIPv6Ranges()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"127.0.0.1", true}, // localhost
|
||||||
|
{"192.168.1.1", true}, // private LAN
|
||||||
|
{"100.64.0.1", true}, // Tailscale
|
||||||
|
{"fd00::1", true}, // local IPv6
|
||||||
|
{"8.8.8.8", false}, // public IPv4
|
||||||
|
{"2001:4860:4860::8888", false}, // public IPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if got := IsPrivateIP(ip); got != tt.expected {
|
||||||
|
t.Errorf("IsPrivateIP(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListContainsIP(t *testing.T) {
|
||||||
|
_, ipNet1, _ := net.ParseCIDR("10.0.0.0/8")
|
||||||
|
_, ipNet2, _ := net.ParseCIDR("192.168.0.0/16")
|
||||||
|
|
||||||
|
list := []*net.IPNet{ipNet1, ipNet2}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"10.1.1.1", true},
|
||||||
|
{"192.168.5.5", true},
|
||||||
|
{"172.16.0.1", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if got := listContainsIP(list, ip); got != tt.expected {
|
||||||
|
t.Errorf("listContainsIP(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_LocalIPv6Ranges(t *testing.T) {
|
||||||
|
// Save and restore env config
|
||||||
|
origRanges := common.EnvConfig.LocalIPv6Ranges
|
||||||
|
defer func() { common.EnvConfig.LocalIPv6Ranges = origRanges }()
|
||||||
|
|
||||||
|
common.EnvConfig.LocalIPv6Ranges = "fd00::/8, invalidCIDR ,fc00::/7"
|
||||||
|
localIPv6Ranges = nil
|
||||||
|
loadLocalIPv6Ranges()
|
||||||
|
|
||||||
|
if len(localIPv6Ranges) != 2 {
|
||||||
|
t.Errorf("expected 2 valid IPv6 ranges, got %d", len(localIPv6Ranges))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,3 +41,14 @@ func (d *JSONDuration) UnmarshalJSON(b []byte) error {
|
|||||||
return errors.New("invalid duration")
|
return errors.New("invalid duration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UnmarshalJSONFromDatabase(data interface{}, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
return json.Unmarshal(v, data)
|
||||||
|
case string:
|
||||||
|
return json.Unmarshal([]byte(v), data)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type: %T", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
205
backend/internal/utils/list_request_util.go
Normal file
205
backend/internal/utils/list_request_util.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaginationResponse struct {
|
||||||
|
TotalPages int64 `json:"totalPages"`
|
||||||
|
TotalItems int64 `json:"totalItems"`
|
||||||
|
CurrentPage int `json:"currentPage"`
|
||||||
|
ItemsPerPage int `json:"itemsPerPage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRequestOptions struct {
|
||||||
|
Pagination struct {
|
||||||
|
Page int `form:"pagination[page]"`
|
||||||
|
Limit int `form:"pagination[limit]"`
|
||||||
|
} `form:"pagination"`
|
||||||
|
Sort struct {
|
||||||
|
Column string `form:"sort[column]"`
|
||||||
|
Direction string `form:"sort[direction]"`
|
||||||
|
} `form:"sort"`
|
||||||
|
Filters map[string][]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldMeta struct {
|
||||||
|
ColumnName string
|
||||||
|
IsSortable bool
|
||||||
|
IsFilterable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOptions) {
|
||||||
|
if err := ctx.ShouldBindQuery(&listRequestOptions); err != nil {
|
||||||
|
return listRequestOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
listRequestOptions.Filters = parseNestedFilters(ctx)
|
||||||
|
return listRequestOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||||
|
meta := extractModelMetadata(result)
|
||||||
|
|
||||||
|
query = applyFilters(params.Filters, query, meta)
|
||||||
|
query = applySorting(params.Sort.Column, params.Sort.Direction, query, meta)
|
||||||
|
|
||||||
|
return Paginate(params.Pagination.Page, params.Pagination.Limit, query, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageSize < 1 {
|
||||||
|
pageSize = 20
|
||||||
|
} else if pageSize > 100 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalItems int64
|
||||||
|
if err := query.Count(&totalItems).Error; err != nil {
|
||||||
|
return PaginationResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
||||||
|
if totalItems == 0 {
|
||||||
|
totalPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(page) > totalPages {
|
||||||
|
page = int(totalPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
||||||
|
return PaginationResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginationResponse{
|
||||||
|
TotalPages: totalPages,
|
||||||
|
TotalItems: totalItems,
|
||||||
|
CurrentPage: page,
|
||||||
|
ItemsPerPage: pageSize,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeSortDirection(direction string) string {
|
||||||
|
d := strings.ToLower(strings.TrimSpace(direction))
|
||||||
|
if d != "asc" && d != "desc" {
|
||||||
|
return "asc"
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidSortDirection(direction string) bool {
|
||||||
|
d := strings.ToLower(strings.TrimSpace(direction))
|
||||||
|
return d == "asc" || d == "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNestedFilters handles ?filters[field][0]=val1&filters[field][1]=val2
|
||||||
|
func parseNestedFilters(ctx *gin.Context) map[string][]any {
|
||||||
|
result := make(map[string][]any)
|
||||||
|
query := ctx.Request.URL.Query()
|
||||||
|
|
||||||
|
for key, values := range query {
|
||||||
|
if !strings.HasPrefix(key, "filters[") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys can be "filters[field]" or "filters[field][0]"
|
||||||
|
raw := strings.TrimPrefix(key, "filters[")
|
||||||
|
// Take everything up to the first closing bracket
|
||||||
|
if idx := strings.IndexByte(raw, ']'); idx != -1 {
|
||||||
|
field := raw[:idx]
|
||||||
|
for _, v := range values {
|
||||||
|
result[field] = append(result[field], ConvertStringToType(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyFilters applies filtering to the GORM query based on the provided filters
|
||||||
|
func applyFilters(filters map[string][]any, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
|
||||||
|
for key, values := range filters {
|
||||||
|
if key == "" || len(values) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldName := CapitalizeFirstLetter(key)
|
||||||
|
fieldMeta, ok := meta[fieldName]
|
||||||
|
if !ok || !fieldMeta.IsFilterable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where(fieldMeta.ColumnName+" IN ?", values)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// applySorting applies sorting to the GORM query based on the provided column and direction
|
||||||
|
func applySorting(sortColumn string, sortDirection string, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
|
||||||
|
fieldName := CapitalizeFirstLetter(sortColumn)
|
||||||
|
fieldMeta, ok := meta[fieldName]
|
||||||
|
if !ok || !fieldMeta.IsSortable {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
sortDirection = NormalizeSortDirection(sortDirection)
|
||||||
|
|
||||||
|
query = query.Clauses(clause.OrderBy{
|
||||||
|
Columns: []clause.OrderByColumn{
|
||||||
|
{Column: clause.Column{Name: fieldMeta.ColumnName}, Desc: sortDirection == "desc"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractModelMetadata extracts FieldMeta from the model struct using reflection
|
||||||
|
func extractModelMetadata(model interface{}) map[string]FieldMeta {
|
||||||
|
meta := make(map[string]FieldMeta)
|
||||||
|
|
||||||
|
// Unwrap pointers and slices to get the element struct type
|
||||||
|
t := reflect.TypeOf(model)
|
||||||
|
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||||
|
t = t.Elem()
|
||||||
|
if t == nil {
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursive parser that merges fields from embedded structs
|
||||||
|
var parseStruct func(reflect.Type)
|
||||||
|
parseStruct = func(st reflect.Type) {
|
||||||
|
for i := 0; i < st.NumField(); i++ {
|
||||||
|
field := st.Field(i)
|
||||||
|
ft := field.Type
|
||||||
|
|
||||||
|
// If the field is an embedded/anonymous struct, recurse into it
|
||||||
|
if field.Anonymous && ft.Kind() == reflect.Struct {
|
||||||
|
parseStruct(ft)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal field: record metadata
|
||||||
|
name := field.Name
|
||||||
|
meta[name] = FieldMeta{
|
||||||
|
ColumnName: CamelCaseToSnakeCase(name),
|
||||||
|
IsSortable: field.Tag.Get("sortable") == "true",
|
||||||
|
IsFilterable: field.Tag.Get("filterable") == "true",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseStruct(t)
|
||||||
|
return meta
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PaginationResponse struct {
|
|
||||||
TotalPages int64 `json:"totalPages"`
|
|
||||||
TotalItems int64 `json:"totalItems"`
|
|
||||||
CurrentPage int `json:"currentPage"`
|
|
||||||
ItemsPerPage int `json:"itemsPerPage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortedPaginationRequest struct {
|
|
||||||
Pagination struct {
|
|
||||||
Page int `form:"pagination[page]"`
|
|
||||||
Limit int `form:"pagination[limit]"`
|
|
||||||
} `form:"pagination"`
|
|
||||||
Sort struct {
|
|
||||||
Column string `form:"sort[column]"`
|
|
||||||
Direction string `form:"sort[direction]"`
|
|
||||||
} `form:"sort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
|
||||||
pagination := sortedPaginationRequest.Pagination
|
|
||||||
sort := sortedPaginationRequest.Sort
|
|
||||||
|
|
||||||
capitalizedSortColumn := CapitalizeFirstLetter(sort.Column)
|
|
||||||
|
|
||||||
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
|
|
||||||
isSortable, _ := strconv.ParseBool(sortField.Tag.Get("sortable"))
|
|
||||||
|
|
||||||
sort.Direction = NormalizeSortDirection(sort.Direction)
|
|
||||||
|
|
||||||
if sortFieldFound && isSortable {
|
|
||||||
columnName := CamelCaseToSnakeCase(sort.Column)
|
|
||||||
query = query.Clauses(clause.OrderBy{
|
|
||||||
Columns: []clause.OrderByColumn{
|
|
||||||
{Column: clause.Column{Name: columnName}, Desc: sort.Direction == "desc"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Paginate(pagination.Page, pagination.Limit, query, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
|
||||||
if page < 1 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if pageSize < 1 {
|
|
||||||
pageSize = 20
|
|
||||||
} else if pageSize > 100 {
|
|
||||||
pageSize = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
|
|
||||||
var totalItems int64
|
|
||||||
if err := query.Count(&totalItems).Error; err != nil {
|
|
||||||
return PaginationResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
|
||||||
return PaginationResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
|
||||||
if totalItems == 0 {
|
|
||||||
totalPages = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return PaginationResponse{
|
|
||||||
TotalPages: totalPages,
|
|
||||||
TotalItems: totalItems,
|
|
||||||
CurrentPage: page,
|
|
||||||
ItemsPerPage: pageSize,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NormalizeSortDirection(direction string) string {
|
|
||||||
d := strings.ToLower(strings.TrimSpace(direction))
|
|
||||||
if d != "asc" && d != "desc" {
|
|
||||||
return "asc"
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsValidSortDirection(direction string) bool {
|
|
||||||
d := strings.ToLower(strings.TrimSpace(direction))
|
|
||||||
return d == "asc" || d == "desc"
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
|
// Ptr returns a pointer to the given value.
|
||||||
func Ptr[T any](v T) *T {
|
func Ptr[T any](v T) *T {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
func PtrValueOrZero[T any](ptr *T) T {
|
// PtrOrNil returns a pointer to v if v is not the zero value of its type,
|
||||||
if ptr == nil {
|
// otherwise it returns nil.
|
||||||
var zero T
|
func PtrOrNil[T comparable](v T) *T {
|
||||||
return zero
|
var zero T
|
||||||
|
if v == zero {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return *ptr
|
return &v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,26 +81,21 @@ func CapitalizeFirstLetter(str string) string {
|
|||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func CamelCaseToSnakeCase(str string) string {
|
var (
|
||||||
result := strings.Builder{}
|
reAcronymBoundary = regexp.MustCompile(`([A-Z]+)([A-Z][a-z])`) // ABCd -> AB_Cd
|
||||||
result.Grow(int(float32(len(str)) * 1.1))
|
reLowerToUpper = regexp.MustCompile(`([a-z0-9])([A-Z])`) // aB -> a_B
|
||||||
for i, r := range str {
|
)
|
||||||
if unicode.IsUpper(r) && i > 0 {
|
|
||||||
result.WriteByte('_')
|
func CamelCaseToSnakeCase(s string) string {
|
||||||
}
|
s = reAcronymBoundary.ReplaceAllString(s, "${1}_${2}")
|
||||||
result.WriteRune(unicode.ToLower(r))
|
s = reLowerToUpper.ReplaceAllString(s, "${1}_${2}")
|
||||||
}
|
return strings.ToLower(s)
|
||||||
return result.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var camelCaseToScreamingSnakeCaseRe = regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
|
||||||
|
|
||||||
func CamelCaseToScreamingSnakeCase(s string) string {
|
func CamelCaseToScreamingSnakeCase(s string) string {
|
||||||
// Insert underscores before uppercase letters (except the first one)
|
s = reAcronymBoundary.ReplaceAllString(s, "${1}_${2}")
|
||||||
snake := camelCaseToScreamingSnakeCaseRe.ReplaceAllString(s, `${1}_${2}`)
|
s = reLowerToUpper.ReplaceAllString(s, "${1}_${2}")
|
||||||
|
return strings.ToUpper(s)
|
||||||
// Convert to uppercase
|
|
||||||
return strings.ToUpper(snake)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFirstCharacter returns the first non-whitespace character of the string, correctly handling Unicode
|
// GetFirstCharacter returns the first non-whitespace character of the string, correctly handling Unicode
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ func TestCamelCaseToSnakeCase(t *testing.T) {
|
|||||||
{"simple camelCase", "camelCase", "camel_case"},
|
{"simple camelCase", "camelCase", "camel_case"},
|
||||||
{"PascalCase", "PascalCase", "pascal_case"},
|
{"PascalCase", "PascalCase", "pascal_case"},
|
||||||
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "multiple_words_in_camel_case"},
|
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "multiple_words_in_camel_case"},
|
||||||
{"consecutive uppercase", "HTTPRequest", "h_t_t_p_request"},
|
{"consecutive uppercase", "HTTPRequest", "http_request"},
|
||||||
{"single lowercase word", "word", "word"},
|
{"single lowercase word", "word", "word"},
|
||||||
{"single uppercase word", "WORD", "w_o_r_d"},
|
{"single uppercase word", "WORD", "word"},
|
||||||
{"with numbers", "camel123Case", "camel123_case"},
|
{"with numbers", "camel123Case", "camel123_case"},
|
||||||
{"with numbers in middle", "model2Name", "model2_name"},
|
{"with numbers in middle", "model2Name", "model2_name"},
|
||||||
{"mixed case", "iPhone6sPlus", "i_phone6s_plus"},
|
{"mixed case", "iPhone6sPlus", "i_phone6s_plus"},
|
||||||
@@ -104,6 +104,34 @@ func TestCamelCaseToSnakeCase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCamelCaseToScreamingSnakeCase(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"empty string", "", ""},
|
||||||
|
{"simple camelCase", "camelCase", "CAMEL_CASE"},
|
||||||
|
{"PascalCase", "PascalCase", "PASCAL_CASE"},
|
||||||
|
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "MULTIPLE_WORDS_IN_CAMEL_CASE"},
|
||||||
|
{"consecutive uppercase", "HTTPRequest", "HTTP_REQUEST"},
|
||||||
|
{"single lowercase word", "word", "WORD"},
|
||||||
|
{"single uppercase word", "WORD", "WORD"},
|
||||||
|
{"with numbers", "camel123Case", "CAMEL123_CASE"},
|
||||||
|
{"with numbers in middle", "model2Name", "MODEL2_NAME"},
|
||||||
|
{"mixed case", "iPhone6sPlus", "I_PHONE6S_PLUS"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := CamelCaseToScreamingSnakeCase(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("CamelCaseToScreamingSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetFirstCharacter(t *testing.T) {
|
func TestGetFirstCharacter(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func NewDatabaseForTest(t *testing.T) *gorm.DB {
|
|||||||
|
|
||||||
// Connect to a new in-memory SQL database
|
// Connect to a new in-memory SQL database
|
||||||
db, err := gorm.Open(
|
db, err := gorm.Open(
|
||||||
sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"),
|
sqlite.Open("file:"+dbName+"?mode=memory"),
|
||||||
&gorm.Config{
|
&gorm.Config{
|
||||||
TranslateError: true,
|
TranslateError: true,
|
||||||
Logger: logger.New(
|
Logger: logger.New(
|
||||||
@@ -52,9 +52,14 @@ func NewDatabaseForTest(t *testing.T) *gorm.DB {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err, "Failed to connect to test database")
|
require.NoError(t, err, "Failed to connect to test database")
|
||||||
|
|
||||||
// Perform migrations with the embedded migrations
|
|
||||||
sqlDB, err := db.DB()
|
sqlDB, err := db.DB()
|
||||||
require.NoError(t, err, "Failed to get sql.DB")
|
require.NoError(t, err, "Failed to get sql.DB")
|
||||||
|
|
||||||
|
// For in-memory SQLite databases, we must limit to 1 open connection at the same time, or they won't see the whole data
|
||||||
|
// The other workaround, of using shared caches, doesn't work well with multiple write transactions trying to happen at once
|
||||||
|
sqlDB.SetMaxOpenConns(1)
|
||||||
|
|
||||||
|
// Perform migrations with the embedded migrations
|
||||||
driver, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{
|
driver, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{
|
||||||
NoTxWrap: true,
|
NoTxWrap: true,
|
||||||
})
|
})
|
||||||
|
|||||||
35
backend/internal/utils/type_util.go
Normal file
35
backend/internal/utils/type_util.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConvertStringToType attempts to convert a string to bool, int, or float.
|
||||||
|
func ConvertStringToType(value string) any {
|
||||||
|
v := strings.TrimSpace(value)
|
||||||
|
if v == "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try bool
|
||||||
|
if v == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if v == "false" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try int
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try float
|
||||||
|
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: string
|
||||||
|
return v
|
||||||
|
}
|
||||||
37
backend/internal/utils/type_util_test.go
Normal file
37
backend/internal/utils/type_util_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertStringToType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected any
|
||||||
|
}{
|
||||||
|
{"true", true},
|
||||||
|
{"false", false},
|
||||||
|
{" true ", true},
|
||||||
|
{" false ", false},
|
||||||
|
{"42", 42},
|
||||||
|
{" 42 ", 42},
|
||||||
|
{"3.14", 3.14},
|
||||||
|
{" 3.14 ", 3.14},
|
||||||
|
{"hello", "hello"},
|
||||||
|
{" hello ", "hello"},
|
||||||
|
{"", ""},
|
||||||
|
{" ", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := ConvertStringToType(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
if f, ok := tt.expected.(float64); ok {
|
||||||
|
if rf, ok := result.(float64); ok && rf == f {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("ConvertStringToType(%q) = %#v (type %T), want %#v (type %T)", tt.input, result, result, tt.expected, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
|||||||
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="width:210px;margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column">
|
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px">
|
||||||
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle;margin-right:8px" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">API Key Expiring Soon</h1></td><td align="right" data-id="__react-email-column">
|
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">API Key Expiring Soon</h1></td><td align="right" data-id="__react-email-column">
|
||||||
<p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Hello <!-- -->{{.Data.Name}}<!-- -->, <br/>This is a reminder that your API key <strong>{{.Data.APIKeyName}}</strong> <!-- -->will expire on <strong>{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}</strong>.</p><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Please generate a new API key if you need continued access.</p></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
<p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Hello <!-- -->{{.Data.Name}}<!-- -->, <br/>This is a reminder that your API key <strong>{{.Data.APIKeyName}}</strong> <!-- -->will expire on <strong>{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}</strong>.</p><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Please generate a new API key if you need continued access.</p></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="width:210px;margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column">
|
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px">
|
||||||
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle;margin-right:8px" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">New Sign-In Detected</h1></td><td align="right" data-id="__react-email-column">
|
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">New Sign-In Detected</h1></td><td align="right" data-id="__react-email-column">
|
||||||
<p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your <!-- -->{{.AppName}}<!-- --> account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.</p><h4 style="font-size:1rem;font-weight:bold;margin:30px 0 10px 0">Details</h4><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Approximate Location</p>
|
<p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your <!-- -->{{.AppName}}<!-- --> account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.</p><h4 style="font-size:1rem;font-weight:bold;margin:30px 0 10px 0">Details</h4><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Approximate Location</p>
|
||||||
<p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.City}}<!-- -->, <!-- -->{{.Data.Country}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">IP Address</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.IPAddress}}</p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:10px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Device</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">
|
<p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">IP Address</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.IPAddress}}</p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:10px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Device</p>
|
||||||
{{.Data.Device}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Sign-In Time</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}</p></td></tr></tbody></table></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
<p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.Device}}</p></td><td data-id="__react-email-column" style="width:225px"><p style="font-size:12px;line-height:24px;margin:0;color:gray;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Sign-In Time</p><p style="font-size:14px;line-height:24px;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}</p></td></tr></tbody></table></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
||||||
@@ -12,7 +12,8 @@ DETAILS
|
|||||||
|
|
||||||
Approximate Location
|
Approximate Location
|
||||||
|
|
||||||
{{.Data.City}}, {{.Data.Country}}
|
{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if
|
||||||
|
.Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}
|
||||||
|
|
||||||
IP Address
|
IP Address
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="width:210px;margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column">
|
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px">
|
||||||
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle;margin-right:8px" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Your Login Code</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">
|
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Your Login Code</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Click the button below to sign in to <!-- -->
|
||||||
Click the button below to sign in to <!-- -->{{.AppName}}<!-- --> with a login code.<br/>Or visit<!-- --> <a href="{{.Data.LoginLink}}" style="color:#000;text-decoration-line:none;text-decoration:underline;font-family:Arial, sans-serif" target="_blank">{{.Data.LoginLink}}</a> <!-- -->and enter the code <strong>{{.Data.Code}}</strong>.<br/><br/>This code expires in <!-- -->{{.Data.ExpirationString}}<!-- -->.</p><div style="text-align:center"><a href="{{.Data.LoginLinkWithCode}}" style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#000000;color:#ffffff;padding:12px 24px;border-radius:4px;font-size:15px;font-weight:500;cursor:pointer;margin-top:10px;padding-top:12px;padding-right:24px;padding-bottom:12px;padding-left:24px" target="_blank"><span><!--[if mso]><i style="mso-font-width:400%;mso-text-raise:18" hidden>   </i><![endif]--></span>
|
{{.AppName}}<!-- --> with a login code.<br/>Or visit<!-- --> <a href="{{.Data.LoginLink}}" style="color:#000;text-decoration-line:none;text-decoration:underline;font-family:Arial, sans-serif" target="_blank">{{.Data.LoginLink}}</a> <!-- -->and enter the code <strong>{{.Data.Code}}</strong>.<br/><br/>This code expires in <!-- -->{{.Data.ExpirationString}}<!-- -->.</p><div style="text-align:center"><a href="{{.Data.LoginLinkWithCode}}" style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#000000;color:#ffffff;padding:12px 24px;border-radius:4px;font-size:15px;font-weight:500;cursor:pointer;margin-top:10px;padding-top:12px;padding-right:24px;padding-bottom:12px;padding-left:24px" target="_blank"><span><!--[if mso]><i style="mso-font-width:400%;mso-text-raise:18" hidden>   </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">
|
||||||
<span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Sign In</span><span><!--[if mso]><i style="mso-font-width:400%" hidden>   ​</i><![endif]--></span></a></div></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
Sign In</span><span><!--[if mso]><i style="mso-font-width:400%" hidden>   ​</i><![endif]--></span></a></div></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="width:210px;margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column">
|
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><!--$--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px">
|
||||||
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle;margin-right:8px" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Test Email</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">
|
<img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">Test Email</h1></td><td align="right" data-id="__react-email-column"></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Your email setup is working correctly!</p></div></td>
|
||||||
Your email setup is working correctly!</p></div></td></tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
</tr></tbody></table><!--7--><!--/$--></body></html>{{end}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- No-op because email was optional before the migration
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oidc_clients DROP COLUMN dark_image_type;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oidc_clients ADD COLUMN dark_image_type TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE webauthn_sessions DROP COLUMN credential_params;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE webauthn_sessions ADD COLUMN credential_params JSONB NOT NULL DEFAULT '[]';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- No-op
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_api_keys_key;
|
||||||
|
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_token;
|
||||||
|
DROP INDEX IF EXISTS idx_signup_tokens_token;
|
||||||
|
DROP INDEX IF EXISTS idx_reauthentication_tokens_token;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- No-op because email was optional before the migration
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
PRAGMA foreign_keys = OFF;
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE users_new
|
||||||
|
(
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME,
|
||||||
|
username TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
||||||
|
email TEXT UNIQUE,
|
||||||
|
first_name TEXT,
|
||||||
|
last_name TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
is_admin NUMERIC NOT NULL DEFAULT FALSE,
|
||||||
|
ldap_id TEXT,
|
||||||
|
locale TEXT,
|
||||||
|
disabled NUMERIC NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO users_new (id, created_at, username, email, first_name, last_name, display_name, is_admin, ldap_id, locale,
|
||||||
|
disabled)
|
||||||
|
SELECT id,
|
||||||
|
created_at,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
display_name,
|
||||||
|
is_admin,
|
||||||
|
ldap_id,
|
||||||
|
locale,
|
||||||
|
disabled
|
||||||
|
FROM users;
|
||||||
|
|
||||||
|
DROP TABLE users;
|
||||||
|
|
||||||
|
ALTER TABLE users_new
|
||||||
|
RENAME TO users;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
BEGIN;
|
||||||
|
ALTER TABLE oidc_clients DROP COLUMN dark_image_type;
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
BEGIN;
|
||||||
|
ALTER TABLE oidc_clients ADD COLUMN dark_image_type TEXT;
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
BEGIN;
|
||||||
|
ALTER TABLE webauthn_sessions DROP COLUMN credential_params;
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
BEGIN;
|
||||||
|
ALTER TABLE webauthn_sessions ADD COLUMN credential_params TEXT NOT NULL DEFAULT '[]';
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
12
cliff.toml
12
cliff.toml
@@ -27,16 +27,12 @@ body = """
|
|||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
### {{ group | title }}
|
### {{ group | title }}
|
||||||
{% for commit in commits %}
|
{% for commit in commits %}
|
||||||
* {{ commit.message }} \
|
- {{ commit.message | trim }} \
|
||||||
{%- if commit.remote.pr_number -%}
|
{%- if commit.remote.pr_number %} ([#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) by @{{ commit.remote.username | default(value=commit.author.name) }}){%- else %} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}) by @{{ commit.remote.username | default(value=commit.author.name) }}){%- endif -%}
|
||||||
([#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) by @{{ commit.remote.username | default(value=commit.author.name) }})
|
|
||||||
{%- else -%}
|
|
||||||
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}) by @{{ commit.remote.username | default(value=commit.author.name) }})
|
|
||||||
{%- endif -%}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if version %}
|
{% if version -%}
|
||||||
{% if previous.version %}
|
{% if previous.version -%}
|
||||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else -%}
|
{% else -%}
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ export const BaseTemplate = ({
|
|||||||
appName,
|
appName,
|
||||||
children,
|
children,
|
||||||
}: BaseTemplateProps) => {
|
}: BaseTemplateProps) => {
|
||||||
const finalLogoURL =
|
|
||||||
logoURL ||
|
|
||||||
"https://private-user-images.githubusercontent.com/58886915/359183039-4ceb2708-9f29-4694-b797-be833efce17d.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTY0NTk5MzksIm5iZiI6MTc1NjQ1OTYzOSwicGF0aCI6Ii81ODg4NjkxNS8zNTkxODMwMzktNGNlYjI3MDgtOWYyOS00Njk0LWI3OTctYmU4MzNlZmNlMTdkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA4MjklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwODI5VDA5MjcxOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWM4ZWI5NzlkMDA5NDNmZGU5MjQwMGE1YjA0NWZiNzEzM2E0MzAzOTFmOWRmNDUzNmJmNjQwZTMxNGIzZmMyYmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.YdfLv1tD5KYnRZPSA3QlR1SsvScpP0rt-J3YD6ZHsCk";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
@@ -32,25 +28,24 @@ export const BaseTemplate = ({
|
|||||||
<Container style={{ width: "500px", margin: "0 auto" }}>
|
<Container style={{ width: "500px", margin: "0 auto" }}>
|
||||||
<Section>
|
<Section>
|
||||||
<Row
|
<Row
|
||||||
align="left"
|
align="left"
|
||||||
style={{
|
style={{
|
||||||
width: "210px",
|
marginBottom: "16px",
|
||||||
marginBottom: "16px",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Column style={{ width: "50px" }}>
|
||||||
<Column>
|
<Img
|
||||||
<Img
|
src={logoURL}
|
||||||
src={finalLogoURL}
|
width="32"
|
||||||
width="32"
|
height="32"
|
||||||
height="32"
|
alt={appName}
|
||||||
alt={appName}
|
style={logoStyle}
|
||||||
style={logoStyle}
|
/>
|
||||||
/>
|
</Column>
|
||||||
</Column>
|
<Column>
|
||||||
<Column>
|
<Text style={titleStyle}>{appName}</Text>
|
||||||
<Text style={titleStyle}>{appName}</Text>
|
</Column>
|
||||||
</Column>
|
</Row>
|
||||||
</Row>
|
|
||||||
</Section>
|
</Section>
|
||||||
<div style={content}>{children}</div>
|
<div style={content}>{children}</div>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -69,7 +64,6 @@ const logoStyle = {
|
|||||||
width: "32px",
|
width: "32px",
|
||||||
height: "32px",
|
height: "32px",
|
||||||
verticalAlign: "middle",
|
verticalAlign: "middle",
|
||||||
marginRight: "8px",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleStyle = {
|
const titleStyle = {
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import CardHeader from "../components/card-header";
|
|||||||
import { sharedPreviewProps, sharedTemplateProps } from "../props";
|
import { sharedPreviewProps, sharedTemplateProps } from "../props";
|
||||||
|
|
||||||
interface SignInData {
|
interface SignInData {
|
||||||
city?: string;
|
location: string;
|
||||||
country?: string;
|
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
device: string;
|
device: string;
|
||||||
dateTime: string;
|
dateTime: string;
|
||||||
@@ -42,9 +41,7 @@ export const NewSignInEmail = ({
|
|||||||
<Row>
|
<Row>
|
||||||
<Column style={detailsBoxStyle}>
|
<Column style={detailsBoxStyle}>
|
||||||
<Text style={detailsLabelStyle}>Approximate Location</Text>
|
<Text style={detailsLabelStyle}>Approximate Location</Text>
|
||||||
<Text style={detailsBoxValueStyle}>
|
<Text style={detailsBoxValueStyle}>{data.location}</Text>
|
||||||
{data.city}, {data.country}
|
|
||||||
</Text>
|
|
||||||
</Column>
|
</Column>
|
||||||
<Column style={detailsBoxStyle}>
|
<Column style={detailsBoxStyle}>
|
||||||
<Text style={detailsLabelStyle}>IP Address</Text>
|
<Text style={detailsLabelStyle}>IP Address</Text>
|
||||||
@@ -84,8 +81,7 @@ const detailsBoxValueStyle = {
|
|||||||
NewSignInEmail.TemplateProps = {
|
NewSignInEmail.TemplateProps = {
|
||||||
...sharedTemplateProps,
|
...sharedTemplateProps,
|
||||||
data: {
|
data: {
|
||||||
city: "{{.Data.City}}",
|
location: "{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}",
|
||||||
country: "{{.Data.Country}}",
|
|
||||||
ipAddress: "{{.Data.IPAddress}}",
|
ipAddress: "{{.Data.IPAddress}}",
|
||||||
device: "{{.Data.Device}}",
|
device: "{{.Data.Device}}",
|
||||||
dateTime: '{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}',
|
dateTime: '{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}',
|
||||||
@@ -95,8 +91,7 @@ NewSignInEmail.TemplateProps = {
|
|||||||
NewSignInEmail.PreviewProps = {
|
NewSignInEmail.PreviewProps = {
|
||||||
...sharedPreviewProps,
|
...sharedPreviewProps,
|
||||||
data: {
|
data: {
|
||||||
city: "San Francisco",
|
location: "San Francisco, USA",
|
||||||
country: "USA",
|
|
||||||
ipAddress: "127.0.0.1",
|
ipAddress: "127.0.0.1",
|
||||||
device: "Chrome on macOS",
|
device: "Chrome on macOS",
|
||||||
dateTime: "2024-01-01 12:00 PM UTC",
|
dateTime: "2024-01-01 12:00 PM UTC",
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "0.1.1",
|
"@react-email/components": "0.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-email/preview-server": "4.2.8",
|
"@react-email/preview-server": "4.2.8",
|
||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.9.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.0.1",
|
"@types/react-dom": "^19.2.2",
|
||||||
"react-email": "4.2.8",
|
"react-email": "4.2.8",
|
||||||
"tsx": "^4.0.0"
|
"tsx": "^4.20.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"use_your_passkey_instead": "Namísto toho použít svůj přístupový klíč?",
|
"use_your_passkey_instead": "Namísto toho použít svůj přístupový klíč?",
|
||||||
"email_login": "Přihlášení e-mailem",
|
"email_login": "Přihlášení e-mailem",
|
||||||
"enter_a_login_code_to_sign_in": "Pro přihlášení zadejte přihlašovací kód.",
|
"enter_a_login_code_to_sign_in": "Pro přihlášení zadejte přihlašovací kód.",
|
||||||
|
"sign_in_with_login_code": "Přihlaste se pomocí přihlašovacího kódu",
|
||||||
"request_a_login_code_via_email": "Požádat o přihlášení pomocí e-mailu.",
|
"request_a_login_code_via_email": "Požádat o přihlášení pomocí e-mailu.",
|
||||||
"go_back": "Jít zpět",
|
"go_back": "Jít zpět",
|
||||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Na zadaný e-mail byl zaslán e-mail, pokud existuje v systému.",
|
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Na zadaný e-mail byl zaslán e-mail, pokud existuje v systému.",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"name_passkey": "Jméno přístupového klíče",
|
"name_passkey": "Jméno přístupového klíče",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Pojmenujte Váš přístupový klíč, abyste ho snadno identifikovali později.",
|
"name_your_passkey_to_easily_identify_it_later": "Pojmenujte Váš přístupový klíč, abyste ho snadno identifikovali později.",
|
||||||
"create_api_key": "Vytvořit API klíč",
|
"create_api_key": "Vytvořit API klíč",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Přidejte nový API klíč pro programový přístup.",
|
"add_a_new_api_key_for_programmatic_access": "Přidejte nový klíč API pro programový přístup k <link href='https://pocket-id.org/docs/api'>API Pocket ID</link>.",
|
||||||
"add_api_key": "Přidat API klíč",
|
"add_api_key": "Přidat API klíč",
|
||||||
"manage_api_keys": "Spravovat API klíče",
|
"manage_api_keys": "Spravovat API klíče",
|
||||||
"api_key_created": "API klíč vytvořen",
|
"api_key_created": "API klíč vytvořen",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Jste si jisti, že chcete zrušit klíč API \"{apiKeyName}\"? To naruší všechny integrace pomocí tohoto klíče.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Jste si jisti, že chcete zrušit klíč API \"{apiKeyName}\"? To naruší všechny integrace pomocí tohoto klíče.",
|
||||||
"last_used": "Naposledy použito",
|
"last_used": "Naposledy použito",
|
||||||
"actions": "Akce",
|
"actions": "Akce",
|
||||||
"images_updated_successfully": "Obrázky úspěšně aktualizovány",
|
"images_updated_successfully": "Obrázky byly úspěšně aktualizovány. Aktualizace může trvat několik minut.",
|
||||||
"general": "Obecné",
|
"general": "Obecné",
|
||||||
"configure_smtp_to_send_emails": "Povolte e-mailová oznámení pro upozornění uživatelů, pokud je zjištěno přihlášení z nového zařízení nebo lokace.",
|
"configure_smtp_to_send_emails": "Povolte e-mailová oznámení pro upozornění uživatelů, pokud je zjištěno přihlášení z nového zařízení nebo lokace.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "Nejsou k dispozici žádná náhledová data",
|
"no_preview_data_available": "Nejsou k dispozici žádná náhledová data",
|
||||||
"copy_all": "Kopírovat vše",
|
"copy_all": "Kopírovat vše",
|
||||||
"preview": "Náhled",
|
"preview": "Náhled",
|
||||||
"preview_for_user": "Náhled pro {name} ({email})",
|
"preview_for_user": "Náhled pro {name}",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Náhled OIDC dat, která by byla odeslána pro uživatele",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Náhled OIDC dat, která by byla odeslána pro uživatele",
|
||||||
"show": "Zobrazit",
|
"show": "Zobrazit",
|
||||||
"select_an_option": "Vyberte možnost",
|
"select_an_option": "Vyberte možnost",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Zobrazované jméno",
|
"display_name": "Zobrazované jméno",
|
||||||
"configure_application_images": "Konfigurace obrazů aplikací",
|
"configure_application_images": "Konfigurace obrazů aplikací",
|
||||||
"ui_config_disabled_info_title": "Konfigurace uživatelského rozhraní je deaktivována",
|
"ui_config_disabled_info_title": "Konfigurace uživatelského rozhraní je deaktivována",
|
||||||
"ui_config_disabled_info_description": "Konfigurace uživatelského rozhraní je deaktivována, protože nastavení konfigurace aplikace se spravuje prostřednictvím proměnných prostředí. Některá nastavení nemusí být editovatelná."
|
"ui_config_disabled_info_description": "Konfigurace uživatelského rozhraní je deaktivována, protože nastavení konfigurace aplikace se spravuje prostřednictvím proměnných prostředí. Některá nastavení nemusí být editovatelná.",
|
||||||
|
"logo_from_url_description": "Vložte přímou URL adresu obrázku (svg, png, webp). Ikony najdete na <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> nebo <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "Neplatná adresa URL",
|
||||||
|
"require_user_email": "Vyžadovat e-mailovou adresu",
|
||||||
|
"require_user_email_description": "Vyžaduje, aby uživatelé měli e-mailovou adresu. Pokud je tato možnost deaktivována, uživatelé bez e-mailové adresy nebudou moci používat funkce, které e-mailovou adresu vyžadují.",
|
||||||
|
"view": "Zobrazení",
|
||||||
|
"toggle_columns": "Přepnout sloupce",
|
||||||
|
"locale": "Jazyk",
|
||||||
|
"ldap_id": "LDAP ID",
|
||||||
|
"reauthentication": "Opětovné ověření",
|
||||||
|
"clear_filters": "Vymazat filtry",
|
||||||
|
"default_profile_picture": "Výchozí profilový obrázek"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"use_your_passkey_instead": "Vil du i stedet bruge din adgangsnøgle?",
|
"use_your_passkey_instead": "Vil du i stedet bruge din adgangsnøgle?",
|
||||||
"email_login": "E-mail Login",
|
"email_login": "E-mail Login",
|
||||||
"enter_a_login_code_to_sign_in": "Indtast en loginkode for at logge ind.",
|
"enter_a_login_code_to_sign_in": "Indtast en loginkode for at logge ind.",
|
||||||
|
"sign_in_with_login_code": "Log ind med login-kode",
|
||||||
"request_a_login_code_via_email": "Anmod om en loginkode via e-mail.",
|
"request_a_login_code_via_email": "Anmod om en loginkode via e-mail.",
|
||||||
"go_back": "Gå tilbage",
|
"go_back": "Gå tilbage",
|
||||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "En e-mail er blevet sendt til den angivne e-mailadresse, hvis den findes i systemet.",
|
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "En e-mail er blevet sendt til den angivne e-mailadresse, hvis den findes i systemet.",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"name_passkey": "Navngiv adgangsnøgle",
|
"name_passkey": "Navngiv adgangsnøgle",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Giv din adgangsnøgle et navn, så du nemt kan genkende den senere.",
|
"name_your_passkey_to_easily_identify_it_later": "Giv din adgangsnøgle et navn, så du nemt kan genkende den senere.",
|
||||||
"create_api_key": "Opret API-nøgle",
|
"create_api_key": "Opret API-nøgle",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Tilføj en ny API-nøgle til programmatisk adgang.",
|
"add_a_new_api_key_for_programmatic_access": "Tilføj en ny API-nøgle for programmatisk adgang til <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>.",
|
||||||
"add_api_key": "Tilføj API-nøgle",
|
"add_api_key": "Tilføj API-nøgle",
|
||||||
"manage_api_keys": "Administrér API-nøgler",
|
"manage_api_keys": "Administrér API-nøgler",
|
||||||
"api_key_created": "API-nøgle oprettet",
|
"api_key_created": "API-nøgle oprettet",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Er du sikker på, at du vil tilbagekalde API-nøglen \"{apiKeyName}\"? Dette vil afbryde alle integrationer, der bruger nøglen.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Er du sikker på, at du vil tilbagekalde API-nøglen \"{apiKeyName}\"? Dette vil afbryde alle integrationer, der bruger nøglen.",
|
||||||
"last_used": "Sidst brugt",
|
"last_used": "Sidst brugt",
|
||||||
"actions": "Handlinger",
|
"actions": "Handlinger",
|
||||||
"images_updated_successfully": "Billeder blev opdateret",
|
"images_updated_successfully": "Billeder opdateret. Det kan tage et par minutter at opdatere.",
|
||||||
"general": "Generelt",
|
"general": "Generelt",
|
||||||
"configure_smtp_to_send_emails": "Aktivér e-mailnotifikationer for at advare brugere, når et login registreres fra en ny enhed eller placering.",
|
"configure_smtp_to_send_emails": "Aktivér e-mailnotifikationer for at advare brugere, når et login registreres fra en ny enhed eller placering.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "Ingen forhåndsvisningsdata tilgængelig",
|
"no_preview_data_available": "Ingen forhåndsvisningsdata tilgængelig",
|
||||||
"copy_all": "Kopiér alt",
|
"copy_all": "Kopiér alt",
|
||||||
"preview": "Forhåndsvisning",
|
"preview": "Forhåndsvisning",
|
||||||
"preview_for_user": "Forhåndsvisning for {name} ({email})",
|
"preview_for_user": "Forhåndsvisning for {name}",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Forhåndsvis OIDC-data, der ville blive sendt for denne bruger",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Forhåndsvis OIDC-data, der ville blive sendt for denne bruger",
|
||||||
"show": "Vis",
|
"show": "Vis",
|
||||||
"select_an_option": "Vælg en indstilling",
|
"select_an_option": "Vælg en indstilling",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Visningsnavn",
|
"display_name": "Visningsnavn",
|
||||||
"configure_application_images": "Konfigurer applikationsbilleder",
|
"configure_application_images": "Konfigurer applikationsbilleder",
|
||||||
"ui_config_disabled_info_title": "UI-konfiguration deaktiveret",
|
"ui_config_disabled_info_title": "UI-konfiguration deaktiveret",
|
||||||
"ui_config_disabled_info_description": "UI-konfigurationen er deaktiveret, fordi applikationskonfigurationsindstillingerne administreres via miljøvariabler. Nogle indstillinger kan muligvis ikke redigeres."
|
"ui_config_disabled_info_description": "UI-konfigurationen er deaktiveret, fordi applikationskonfigurationsindstillingerne administreres via miljøvariabler. Nogle indstillinger kan muligvis ikke redigeres.",
|
||||||
|
"logo_from_url_description": "Indsæt en direkte billed-URL (svg, png, webp). Find ikoner på <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> eller <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "Ugyldig URL",
|
||||||
|
"require_user_email": "Kræver e-mailadresse",
|
||||||
|
"require_user_email_description": "Kræver, at brugerne har en e-mailadresse. Hvis denne funktion er deaktiveret, kan brugere uden en e-mailadresse ikke bruge funktioner, der kræver en e-mailadresse.",
|
||||||
|
"view": "Vis",
|
||||||
|
"toggle_columns": "Skift kolonner",
|
||||||
|
"locale": "Lokale",
|
||||||
|
"ldap_id": "LDAP-id",
|
||||||
|
"reauthentication": "Genbekræftelse",
|
||||||
|
"clear_filters": "Ryd filtre",
|
||||||
|
"default_profile_picture": "Standardprofilbillede"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"use_your_passkey_instead": "Deinen Passkey stattdessen verwenden?",
|
"use_your_passkey_instead": "Deinen Passkey stattdessen verwenden?",
|
||||||
"email_login": "E-Mail Anmeldung",
|
"email_login": "E-Mail Anmeldung",
|
||||||
"enter_a_login_code_to_sign_in": "Gib einen Anmeldecode zum Anmelden ein.",
|
"enter_a_login_code_to_sign_in": "Gib einen Anmeldecode zum Anmelden ein.",
|
||||||
|
"sign_in_with_login_code": "Mit Login-Code anmelden",
|
||||||
"request_a_login_code_via_email": "Login-Code per E-Mail anfordern.",
|
"request_a_login_code_via_email": "Login-Code per E-Mail anfordern.",
|
||||||
"go_back": "Zurück",
|
"go_back": "Zurück",
|
||||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Eine E-Mail wurde an die angegebene E-Mail gesendet, sofern sie im System vorhanden ist.",
|
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Eine E-Mail wurde an die angegebene E-Mail gesendet, sofern sie im System vorhanden ist.",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"name_passkey": "Passkey benennen",
|
"name_passkey": "Passkey benennen",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Benenne deinen Passkey, um ihn später leicht identifizieren zu können.",
|
"name_your_passkey_to_easily_identify_it_later": "Benenne deinen Passkey, um ihn später leicht identifizieren zu können.",
|
||||||
"create_api_key": "API-Schlüssel erstellen",
|
"create_api_key": "API-Schlüssel erstellen",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Füge einen neuen API-Schlüssel für programmatischen Zugriff hinzu.",
|
"add_a_new_api_key_for_programmatic_access": "Füge einen neuen API-Schlüssel für den programmgesteuerten Zugriff auf die <link href='https://pocket-id.org/docs/api'>Pocket ID API</link> hinzu.",
|
||||||
"add_api_key": "API-Schlüssel hinzufügen",
|
"add_api_key": "API-Schlüssel hinzufügen",
|
||||||
"manage_api_keys": "API-Schlüssel verwalten",
|
"manage_api_keys": "API-Schlüssel verwalten",
|
||||||
"api_key_created": "API-Schlüssel erstellt",
|
"api_key_created": "API-Schlüssel erstellt",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bist du sicher, dass du den API-Schlüssel \"{apiKeyName}\" widerrufen willst? Das wird jegliche Integrationen, die diesen Schlüssel verwenden, brechen.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bist du sicher, dass du den API-Schlüssel \"{apiKeyName}\" widerrufen willst? Das wird jegliche Integrationen, die diesen Schlüssel verwenden, brechen.",
|
||||||
"last_used": "Letzte Verwendung",
|
"last_used": "Letzte Verwendung",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"images_updated_successfully": "Bild erfolgreich aktualisiert",
|
"images_updated_successfully": "Bilder erfolgreich aktualisiert. Die Aktualisierung kann ein paar Minuten dauern.",
|
||||||
"general": "Allgemein",
|
"general": "Allgemein",
|
||||||
"configure_smtp_to_send_emails": "Aktiviere E-Mail Benachrichtigungen, um Benutzer zu informieren, wenn ein Login von einem neuen Gerät oder Standort erkannt wird.",
|
"configure_smtp_to_send_emails": "Aktiviere E-Mail Benachrichtigungen, um Benutzer zu informieren, wenn ein Login von einem neuen Gerät oder Standort erkannt wird.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "Keine Vorschaudaten verfügbar",
|
"no_preview_data_available": "Keine Vorschaudaten verfügbar",
|
||||||
"copy_all": "Alles kopieren",
|
"copy_all": "Alles kopieren",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"preview_for_user": "Vorschau für {name} ({email})",
|
"preview_for_user": "Vorschau für {name}",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Vorschau der OIDC-Daten, für diesen Benutzer",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Vorschau der OIDC-Daten, für diesen Benutzer",
|
||||||
"show": "Anzeigen",
|
"show": "Anzeigen",
|
||||||
"select_an_option": "Wähle eine Option",
|
"select_an_option": "Wähle eine Option",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Anzeigename",
|
"display_name": "Anzeigename",
|
||||||
"configure_application_images": "Anwendungsimages einrichten",
|
"configure_application_images": "Anwendungsimages einrichten",
|
||||||
"ui_config_disabled_info_title": "UI-Konfiguration deaktiviert",
|
"ui_config_disabled_info_title": "UI-Konfiguration deaktiviert",
|
||||||
"ui_config_disabled_info_description": "Die UI-Konfiguration ist deaktiviert, weil die Anwendungseinstellungen über Umgebungsvariablen verwaltet werden. Manche Einstellungen können vielleicht nicht geändert werden."
|
"ui_config_disabled_info_description": "Die UI-Konfiguration ist deaktiviert, weil die Anwendungseinstellungen über Umgebungsvariablen verwaltet werden. Manche Einstellungen können vielleicht nicht geändert werden.",
|
||||||
|
"logo_from_url_description": "Füge eine direkte Bild-URL ein (svg, png, webp). Finde Symbole bei <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> oder <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "Ungültige URL",
|
||||||
|
"require_user_email": "E-Mail-Adresse erforderlich",
|
||||||
|
"require_user_email_description": "Benutzer müssen eine E-Mail-Adresse haben. Wenn das deaktiviert ist, können Leute ohne E-Mail-Adresse die Funktionen, die eine E-Mail-Adresse brauchen, nicht nutzen.",
|
||||||
|
"view": "Ansicht",
|
||||||
|
"toggle_columns": "Spalten umschalten",
|
||||||
|
"locale": "Ort",
|
||||||
|
"ldap_id": "LDAP-ID",
|
||||||
|
"reauthentication": "Erneute Authentifizierung",
|
||||||
|
"clear_filters": "Filter löschen",
|
||||||
|
"default_profile_picture": "Standard-Profilbild"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"use_your_passkey_instead": "Use your passkey instead?",
|
"use_your_passkey_instead": "Use your passkey instead?",
|
||||||
"email_login": "Email Login",
|
"email_login": "Email Login",
|
||||||
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
||||||
|
"sign_in_with_login_code": "Sign in with login code",
|
||||||
"request_a_login_code_via_email": "Request a login code via email.",
|
"request_a_login_code_via_email": "Request a login code via email.",
|
||||||
"go_back": "Go back",
|
"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.",
|
"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.",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"name_passkey": "Name Passkey",
|
"name_passkey": "Name Passkey",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.",
|
"name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.",
|
||||||
"create_api_key": "Create API Key",
|
"create_api_key": "Create API Key",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access.",
|
"add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access to the <link href='https://pocket-id.org/docs/api'>Pocket ID API</link>.",
|
||||||
"add_api_key": "Add API Key",
|
"add_api_key": "Add API Key",
|
||||||
"manage_api_keys": "Manage API Keys",
|
"manage_api_keys": "Manage API Keys",
|
||||||
"api_key_created": "API Key Created",
|
"api_key_created": "API Key Created",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.",
|
||||||
"last_used": "Last Used",
|
"last_used": "Last Used",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"images_updated_successfully": "Images updated successfully",
|
"images_updated_successfully": "Images updated successfully. It may take a few minutes to update.",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "No preview data available",
|
"no_preview_data_available": "No preview data available",
|
||||||
"copy_all": "Copy All",
|
"copy_all": "Copy All",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"preview_for_user": "Preview for {name} ({email})",
|
"preview_for_user": "Preview for {name}",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"select_an_option": "Select an option",
|
"select_an_option": "Select an option",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Display Name",
|
"display_name": "Display Name",
|
||||||
"configure_application_images": "Configure Application Images",
|
"configure_application_images": "Configure Application Images",
|
||||||
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||||
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable."
|
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.",
|
||||||
|
"logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> or <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "Invalid URL",
|
||||||
|
"require_user_email": "Require Email Address",
|
||||||
|
"require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address.",
|
||||||
|
"view": "View",
|
||||||
|
"toggle_columns": "Toggle columns",
|
||||||
|
"locale": "Locale",
|
||||||
|
"ldap_id": "LDAP ID",
|
||||||
|
"reauthentication": "Re-authentication",
|
||||||
|
"clear_filters": "Clear Filters",
|
||||||
|
"default_profile_picture": "Default Profile Picture"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"use_your_passkey_instead": "¿Utilizar su Passkey en su lugar?",
|
"use_your_passkey_instead": "¿Utilizar su Passkey en su lugar?",
|
||||||
"email_login": "Ingreso con Email",
|
"email_login": "Ingreso con Email",
|
||||||
"enter_a_login_code_to_sign_in": "Introduzca un código de acceso para iniciar sesión.",
|
"enter_a_login_code_to_sign_in": "Introduzca un código de acceso para iniciar sesión.",
|
||||||
|
"sign_in_with_login_code": "Inicia sesión con tu código de acceso.",
|
||||||
"request_a_login_code_via_email": "Solicitar un código de acceso por correo electrónico.",
|
"request_a_login_code_via_email": "Solicitar un código de acceso por correo electrónico.",
|
||||||
"go_back": "Volver atrás",
|
"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.",
|
"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.",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"name_passkey": "Nombre para la clave de acceso",
|
"name_passkey": "Nombre para la clave de acceso",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Nombra tu clave de acceso para poder identificarla fácilmente más tarde.",
|
"name_your_passkey_to_easily_identify_it_later": "Nombra tu clave de acceso para poder identificarla fácilmente más tarde.",
|
||||||
"create_api_key": "Crear API Key",
|
"create_api_key": "Crear API Key",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Añade una nueva API key para el acceso programático.",
|
"add_a_new_api_key_for_programmatic_access": "Añade una nueva clave API para acceder de forma programática a la <link href='https://pocket-id.org/docs/api'>API de Pocket ID</link>.",
|
||||||
"add_api_key": "Añade una API Key",
|
"add_api_key": "Añade una API Key",
|
||||||
"manage_api_keys": "Gestiona las API Keys",
|
"manage_api_keys": "Gestiona las API Keys",
|
||||||
"api_key_created": "API Key creada",
|
"api_key_created": "API Key creada",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "¿Estás seguro de que deseas invalidar la API Key \"{apiKeyName}\"? Esto romperá cualquier integración que esté usando esta clave.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "¿Estás seguro de que deseas invalidar la API Key \"{apiKeyName}\"? Esto romperá cualquier integración que esté usando esta clave.",
|
||||||
"last_used": "Utilizado por última vez",
|
"last_used": "Utilizado por última vez",
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
"images_updated_successfully": "Imágenes actualizadas correctamente",
|
"images_updated_successfully": "Imágenes actualizadas correctamente. La actualización puede tardar unos minutos.",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"configure_smtp_to_send_emails": "Habilita las notificaciones por correo electrónico para alertar a los usuarios cuando se detecta un inicio de sesión desde un nuevo dispositivo o ubicación.",
|
"configure_smtp_to_send_emails": "Habilita las notificaciones por correo electrónico para alertar a los usuarios cuando se detecta un inicio de sesión desde un nuevo dispositivo o ubicación.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "No hay datos de vista previa disponibles.",
|
"no_preview_data_available": "No hay datos de vista previa disponibles.",
|
||||||
"copy_all": "Copiar todo",
|
"copy_all": "Copiar todo",
|
||||||
"preview": "Vista previa",
|
"preview": "Vista previa",
|
||||||
"preview_for_user": "Vista previa de « {name} » ({email})",
|
"preview_for_user": "Vista previa de « {name} »",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Previsualiza los datos OIDC que se enviarían para este usuario.",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Previsualiza los datos OIDC que se enviarían para este usuario.",
|
||||||
"show": "Mostrar",
|
"show": "Mostrar",
|
||||||
"select_an_option": "Selecciona una opción",
|
"select_an_option": "Selecciona una opción",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Nombre para mostrar",
|
"display_name": "Nombre para mostrar",
|
||||||
"configure_application_images": "Configurar imágenes de aplicaciones",
|
"configure_application_images": "Configurar imágenes de aplicaciones",
|
||||||
"ui_config_disabled_info_title": "Configuración de la interfaz de usuario desactivada",
|
"ui_config_disabled_info_title": "Configuración de la interfaz de usuario desactivada",
|
||||||
"ui_config_disabled_info_description": "La configuración de la interfaz de usuario está desactivada porque los ajustes de configuración de la aplicación se gestionan a través de variables de entorno. Es posible que algunos ajustes no se puedan editar."
|
"ui_config_disabled_info_description": "La configuración de la interfaz de usuario está desactivada porque los ajustes de configuración de la aplicación se gestionan a través de variables de entorno. Es posible que algunos ajustes no se puedan editar.",
|
||||||
|
"logo_from_url_description": "Pega una URL de imagen directa (svg, png, webp). Encuentra iconos en <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "URL no válida",
|
||||||
|
"require_user_email": "Requerir dirección de correo electrónico",
|
||||||
|
"require_user_email_description": "Requiere que los usuarios tengan una dirección de correo electrónico. Si se desactiva, los usuarios que no tengan una dirección de correo electrónico no podrán utilizar las funciones que la requieran.",
|
||||||
|
"view": "Ver",
|
||||||
|
"toggle_columns": "Alternar columnas",
|
||||||
|
"locale": "Configuración regional",
|
||||||
|
"ldap_id": "Identificador LDAP",
|
||||||
|
"reauthentication": "Reautenticación",
|
||||||
|
"clear_filters": "Borrar filtros",
|
||||||
|
"default_profile_picture": "Imagen de perfil predeterminada"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@
|
|||||||
"image_should_be_in_format": "L'image doit être au format PNG ou JPEG.",
|
"image_should_be_in_format": "L'image doit être au format PNG ou JPEG.",
|
||||||
"items_per_page": "Éléments par page",
|
"items_per_page": "Éléments par page",
|
||||||
"no_items_found": "Aucune donnée trouvée",
|
"no_items_found": "Aucune donnée trouvée",
|
||||||
"select_items": "Choisis des trucs...",
|
"select_items": "Sélectionner des éléments...",
|
||||||
"search": "Rechercher...",
|
"search": "Rechercher...",
|
||||||
"expand_card": "Carte d'expansion",
|
"expand_card": "Développer la carte",
|
||||||
"copied": "Copié",
|
"copied": "Copié",
|
||||||
"click_to_copy": "Cliquer pour copier",
|
"click_to_copy": "Cliquer pour copier",
|
||||||
"something_went_wrong": "Quelque chose n'a pas fonctionné",
|
"something_went_wrong": "Quelque chose n'a pas fonctionné",
|
||||||
"go_back_to_home": "Retourner à l'accueil",
|
"go_back_to_home": "Retourner à l'accueil",
|
||||||
"alternative_sign_in_methods": "Autres façons de se connecter",
|
"alternative_sign_in_methods": "Méthodes de connexion alternatives",
|
||||||
"login_background": "Arrière-plan de connexion",
|
"login_background": "Arrière-plan de connexion",
|
||||||
"logo": "Logo",
|
"logo": "Logo",
|
||||||
"login_code": "Code de connexion",
|
"login_code": "Code de connexion",
|
||||||
@@ -75,9 +75,10 @@
|
|||||||
"use_your_passkey_instead": "Utiliser votre clé d'accès à la place ?",
|
"use_your_passkey_instead": "Utiliser votre clé d'accès à la place ?",
|
||||||
"email_login": "Connexion par e-mail",
|
"email_login": "Connexion par e-mail",
|
||||||
"enter_a_login_code_to_sign_in": "Entrez un code de connexion pour vous connecter.",
|
"enter_a_login_code_to_sign_in": "Entrez un code de connexion pour vous connecter.",
|
||||||
|
"sign_in_with_login_code": "Connectez-vous avec votre code d'accès",
|
||||||
"request_a_login_code_via_email": "Demander un code de connexion par e-mail.",
|
"request_a_login_code_via_email": "Demander un code de connexion par e-mail.",
|
||||||
"go_back": "Retour",
|
"go_back": "Retour",
|
||||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Un e-mail a été envoyé à l'e-mail mentionné, si elle existe dans le système.",
|
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Un e-mail a été envoyé à l'adresse mentionnée, si celle-ci existe dans le système.",
|
||||||
"enter_code": "Entrez le code",
|
"enter_code": "Entrez le code",
|
||||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Entrez votre adresse e-mail pour recevoir un email avec un code de connexion.",
|
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Entrez votre adresse e-mail pour recevoir un email avec un code de connexion.",
|
||||||
"your_email": "Votre email",
|
"your_email": "Votre email",
|
||||||
@@ -89,10 +90,10 @@
|
|||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"user_groups": "Groupes d’utilisateurs",
|
"user_groups": "Groupes d’utilisateurs",
|
||||||
"oidc_clients": "Clients OIDC",
|
"oidc_clients": "Clients OIDC",
|
||||||
"api_keys": "Clés API",
|
"api_keys": "Clés d'API",
|
||||||
"application_configuration": "Configuration de l’application",
|
"application_configuration": "Configuration de l’application",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"update_pocket_id": "Mise à jour de Pocket ID",
|
"update_pocket_id": "Mettre à jour Pocket ID",
|
||||||
"powered_by": "Propulsé par",
|
"powered_by": "Propulsé par",
|
||||||
"see_your_account_activities_from_the_last_3_months": "Consultez les activités de votre compte au cours des 3 derniers mois.",
|
"see_your_account_activities_from_the_last_3_months": "Consultez les activités de votre compte au cours des 3 derniers mois.",
|
||||||
"time": "Date et heure",
|
"time": "Date et heure",
|
||||||
@@ -134,11 +135,11 @@
|
|||||||
"name_passkey": "Nom de la clé d'accès",
|
"name_passkey": "Nom de la clé d'accès",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Nommez votre clé d'accès pour l'identifier plus tard.",
|
"name_your_passkey_to_easily_identify_it_later": "Nommez votre clé d'accès pour l'identifier plus tard.",
|
||||||
"create_api_key": "Créer une clé API",
|
"create_api_key": "Créer une clé API",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Ajouter une nouvelle clé API pour l'accès par des programmes tiers.",
|
"add_a_new_api_key_for_programmatic_access": "Ajoute une nouvelle clé API pour accéder de manière programmatique à <link href='https://pocket-id.org/docs/api'>l'API Pocket ID</link>.",
|
||||||
"add_api_key": "Crée une clé API",
|
"add_api_key": "Crée une clé API",
|
||||||
"manage_api_keys": "Gérer les clés API",
|
"manage_api_keys": "Gérer les clés API",
|
||||||
"api_key_created": "Clé API créée",
|
"api_key_created": "Clé API créée",
|
||||||
"for_security_reasons_this_key_will_only_be_shown_once": "Pour des raisons de sécurité, cette clé ne sera affichée qu'une seule fois. Veuillez la conserver en toute sécurité.",
|
"for_security_reasons_this_key_will_only_be_shown_once": "Pour des raisons de sécurité, cette clé ne sera affichée qu'une seule fois. Veuillez la conserver dans un endroit sûr.",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"api_key": "Clé API",
|
"api_key": "Clé API",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Êtes-vous sûr de vouloir révoquer la clé API \"{apiKeyName}\" ? Cela va casser toutes les intégrations utilisant cette clé.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Êtes-vous sûr de vouloir révoquer la clé API \"{apiKeyName}\" ? Cela va casser toutes les intégrations utilisant cette clé.",
|
||||||
"last_used": "Dernière utilisation",
|
"last_used": "Dernière utilisation",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"images_updated_successfully": "Image mise à jour avec succès",
|
"images_updated_successfully": "Les images ont été mises à jour sans problème. Ça peut prendre quelques minutes pour que tout se mette à jour.",
|
||||||
"general": "Général",
|
"general": "Général",
|
||||||
"configure_smtp_to_send_emails": "Activer les notifications par e-mail pour alerter les utilisateurs lorsqu'une connexion est détectée à partir d'un nouvel appareil ou d'un nouvel emplacement.",
|
"configure_smtp_to_send_emails": "Activer les notifications par e-mail pour alerter les utilisateurs lorsqu'une connexion est détectée à partir d'un nouvel appareil ou d'un nouvel emplacement.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -162,7 +163,7 @@
|
|||||||
"images": "Images",
|
"images": "Images",
|
||||||
"update": "Mise à jour",
|
"update": "Mise à jour",
|
||||||
"email_configuration_updated_successfully": "La configuration du serveur mail à été mise à jour avec succès",
|
"email_configuration_updated_successfully": "La configuration du serveur mail à été mise à jour avec succès",
|
||||||
"save_changes_question": "Enregistrer des changements ?",
|
"save_changes_question": "Enregistrer ces changements ?",
|
||||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Vous devez enregistrer les modifications avant d'envoyer un e-mail de test. Voulez-vous enregistrer maintenant ?",
|
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Vous devez enregistrer les modifications avant d'envoyer un e-mail de test. Voulez-vous enregistrer maintenant ?",
|
||||||
"save_and_send": "Enregistrer et envoyer",
|
"save_and_send": "Enregistrer et envoyer",
|
||||||
"test_email_sent_successfully": "L'e-mail de test a été envoyé avec succès à votre adresse e-mail.",
|
"test_email_sent_successfully": "L'e-mail de test a été envoyé avec succès à votre adresse e-mail.",
|
||||||
@@ -255,7 +256,7 @@
|
|||||||
"manage_user_groups": "Gérer les groupes d'utilisateurs",
|
"manage_user_groups": "Gérer les groupes d'utilisateurs",
|
||||||
"friendly_name": "Nom d'affichage",
|
"friendly_name": "Nom d'affichage",
|
||||||
"name_that_will_be_displayed_in_the_ui": "Nom qui sera affiché dans l'interface utilisateur",
|
"name_that_will_be_displayed_in_the_ui": "Nom qui sera affiché dans l'interface utilisateur",
|
||||||
"name_that_will_be_in_the_groups_claim": "Nommez ce qui sera dans le \"groupe\" claim",
|
"name_that_will_be_in_the_groups_claim": "Nom qui sera dans la revendication \"groups\"",
|
||||||
"delete_name": "Supprimer {name}",
|
"delete_name": "Supprimer {name}",
|
||||||
"are_you_sure_you_want_to_delete_this_user_group": "Êtes-vous sûr de vouloir supprimer ce groupe d'utilisateurs?",
|
"are_you_sure_you_want_to_delete_this_user_group": "Êtes-vous sûr de vouloir supprimer ce groupe d'utilisateurs?",
|
||||||
"user_group_deleted_successfully": "Groupe d'utilisateurs supprimé avec succès",
|
"user_group_deleted_successfully": "Groupe d'utilisateurs supprimé avec succès",
|
||||||
@@ -271,14 +272,14 @@
|
|||||||
"add_oidc_client": "Ajouter un client OIDC",
|
"add_oidc_client": "Ajouter un client OIDC",
|
||||||
"manage_oidc_clients": "Gérer les clients OIDC",
|
"manage_oidc_clients": "Gérer les clients OIDC",
|
||||||
"one_time_link": "Lien de connexion unique",
|
"one_time_link": "Lien de connexion unique",
|
||||||
"use_this_link_to_sign_in_once": "Utilisez ce lien pour vous connecter. Ceci est nécessaire pour les utilisateurs qui n'ont pas encore ajouté de clé d'accès ou l'ont perdu.",
|
"use_this_link_to_sign_in_once": "Utilisez ce lien pour vous connecter. Ceci est nécessaire pour les utilisateurs qui n'ont pas encore ajouté de clé d'accès ou qui l'ont perdu.",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"callback_urls": "URL de callback",
|
"callback_urls": "URL de callback",
|
||||||
"logout_callback_urls": "URL de callback de déconnexion",
|
"logout_callback_urls": "URL de callback de déconnexion",
|
||||||
"public_client": "Client public",
|
"public_client": "Client public",
|
||||||
"public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
|
"public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
|
||||||
"pkce": "PKCE",
|
"pkce": "PKCE",
|
||||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et l’interception de code d’autorisation.",
|
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et d’interception de code d’autorisation.",
|
||||||
"requires_reauthentication": "Nécessite une nouvelle authentification",
|
"requires_reauthentication": "Nécessite une nouvelle authentification",
|
||||||
"requires_users_to_authenticate_again_on_each_authorization": "Demande aux utilisateurs de se connecter à nouveau à chaque autorisation, même s'ils sont déjà connectés.",
|
"requires_users_to_authenticate_again_on_each_authorization": "Demande aux utilisateurs de se connecter à nouveau à chaque autorisation, même s'ils sont déjà connectés.",
|
||||||
"name_logo": "Logo {name}",
|
"name_logo": "Logo {name}",
|
||||||
@@ -317,10 +318,10 @@
|
|||||||
"reset": "Réinitialiser",
|
"reset": "Réinitialiser",
|
||||||
"reset_to_default": "Valeurs par défaut",
|
"reset_to_default": "Valeurs par défaut",
|
||||||
"profile_picture_has_been_reset": "La photo de profil a été réinitialisée. La mise à jour peut prendre quelques minutes.",
|
"profile_picture_has_been_reset": "La photo de profil a été réinitialisée. La mise à jour peut prendre quelques minutes.",
|
||||||
"select_the_language_you_want_to_use": "Choisis la langue que tu veux utiliser. Attention, certains textes peuvent être traduits automatiquement et ne pas être tout à fait exacts.",
|
"select_the_language_you_want_to_use": "Choisissez la langue que vous souhaitez utiliser. Attention, certains textes peuvent être traduits automatiquement et ne pas être tout à fait exacts.",
|
||||||
"contribute_to_translation": "Si tu trouves un problème, n'hésite pas à contribuer à la traduction sur <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
"contribute_to_translation": "Si vous trouvez un problème, n'hésitez pas à contribuer à la traduction sur <link href='https://crowdin.com/project/pocket-id'>Crowdin</link>.",
|
||||||
"personal": "Personnel",
|
"personal": "Personnel",
|
||||||
"global": "Mondial",
|
"global": "Global",
|
||||||
"all_users": "Tous les utilisateurs",
|
"all_users": "Tous les utilisateurs",
|
||||||
"all_events": "Tous les événements",
|
"all_events": "Tous les événements",
|
||||||
"all_clients": "Tous les clients",
|
"all_clients": "Tous les clients",
|
||||||
@@ -347,7 +348,7 @@
|
|||||||
"callback_url_description": "URL(s) fournies par votre client. Sera automatiquement ajoutée si laissée vide. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
|
"callback_url_description": "URL(s) fournies par votre client. Sera automatiquement ajoutée si laissée vide. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
|
||||||
"logout_callback_url_description": "URL(s) fournies par votre client pour la déconnexion. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
|
"logout_callback_url_description": "URL(s) fournies par votre client pour la déconnexion. Les jokers (*) sont supportés, mais il est préférable de les éviter pour plus de sécurité.",
|
||||||
"api_key_expiration": "Expiration de la clé API",
|
"api_key_expiration": "Expiration de la clé API",
|
||||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envoyer un email à l'utilisateur lorsque sa clé API est sur le point d'expirer.",
|
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Envoyer un email à l'utilisateur lorsque sa clé d'API est sur le point d'expirer.",
|
||||||
"authorize_device": "Autoriser l'appareil",
|
"authorize_device": "Autoriser l'appareil",
|
||||||
"the_device_has_been_authorized": "L'appareil a été autorisé.",
|
"the_device_has_been_authorized": "L'appareil a été autorisé.",
|
||||||
"enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.",
|
"enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "Aucune donnée d'aperçu disponible",
|
"no_preview_data_available": "Aucune donnée d'aperçu disponible",
|
||||||
"copy_all": "Tout copier",
|
"copy_all": "Tout copier",
|
||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"preview_for_user": "Aperçu pour {name} ({email})",
|
"preview_for_user": "Aperçu pour {name}",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Aperçu des données OIDC qui seraient envoyées pour cet utilisateur",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Aperçu des données OIDC qui seraient envoyées pour cet utilisateur",
|
||||||
"show": "Afficher",
|
"show": "Afficher",
|
||||||
"select_an_option": "Sélectionner une option",
|
"select_an_option": "Sélectionner une option",
|
||||||
@@ -381,7 +382,7 @@
|
|||||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Sélectionnez une couleur d'accent pour personnaliser l'apparence de Pocket ID.",
|
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Sélectionnez une couleur d'accent pour personnaliser l'apparence de Pocket ID.",
|
||||||
"accent_color": "Couleur d'accent",
|
"accent_color": "Couleur d'accent",
|
||||||
"custom_accent_color": "Couleur d'accent personnalisée",
|
"custom_accent_color": "Couleur d'accent personnalisée",
|
||||||
"custom_accent_color_description": "Entrez une couleur personnalisée en utilisant un format CSS valide (par ex. hex, rgb, hsl).",
|
"custom_accent_color_description": "Entrez une couleur personnalisée en utilisant un format CSS valide (ex. : hex, rgb, hsl).",
|
||||||
"color_value": "Valeur de la couleur",
|
"color_value": "Valeur de la couleur",
|
||||||
"apply": "Appliquer",
|
"apply": "Appliquer",
|
||||||
"signup_token": "Jeton d'inscription",
|
"signup_token": "Jeton d'inscription",
|
||||||
@@ -428,19 +429,19 @@
|
|||||||
"signup_open": "Inscription ouverte",
|
"signup_open": "Inscription ouverte",
|
||||||
"signup_open_description": "Toute personne peut créer un nouveau compte sans restriction.",
|
"signup_open_description": "Toute personne peut créer un nouveau compte sans restriction.",
|
||||||
"of": "sur",
|
"of": "sur",
|
||||||
"skip_passkey_setup": "Ignorer la configuration de la clé d'accès",
|
"skip_passkey_setup": "Ignorer la configuration d'une clé d'accès",
|
||||||
"skip_passkey_setup_description": "Il est fortement recommandé de configurer une clé d'accès, car sans elle, vous serez verrouillé hors de votre compte dès l'expiration de la session.",
|
"skip_passkey_setup_description": "Il est fortement recommandé de configurer une clé d'accès, car sans elle, vous serez verrouillé hors de votre compte dès l'expiration de la session.",
|
||||||
"my_apps": "Mes applications",
|
"my_apps": "Mes applications",
|
||||||
"no_apps_available": "Aucune appli disponible",
|
"no_apps_available": "Aucune app disponible",
|
||||||
"contact_your_administrator_for_app_access": "Contacte ton administrateur pour avoir accès aux applications.",
|
"contact_your_administrator_for_app_access": "Contactez votre administrateur pour avoir accès aux applications.",
|
||||||
"launch": "Lancement",
|
"launch": "Lancement",
|
||||||
"client_launch_url": "URL de lancement du client",
|
"client_launch_url": "URL de lancement du client",
|
||||||
"client_launch_url_description": "L'URL qui s'ouvrira quand quelqu'un lancera l'appli depuis la page Mes applis.",
|
"client_launch_url_description": "L'URL qui s'ouvrira quand quelqu'un lancera l'appli depuis la page Mes applis.",
|
||||||
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
|
"client_name_description": "Le nom du client qui apparaît dans l'interface utilisateur Pocket ID.",
|
||||||
"revoke_access": "Supprimer l'accès",
|
"revoke_access": "Supprimer l'accès",
|
||||||
"revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de ton compte.",
|
"revoke_access_description": "Supprimer l'accès à <b>{clientName}</b>. <b>{clientName}</b> ne pourra plus accéder aux infos de votre compte.",
|
||||||
"revoke_access_successful": "L'accès à {clientName} a été supprimé.",
|
"revoke_access_successful": "L'accès à {clientName} a été supprimé.",
|
||||||
"last_signed_in_ago": "Dernière connexion il y a {time} il y a",
|
"last_signed_in_ago": "Dernière connexion il y a {time}",
|
||||||
"invalid_client_id": "L'ID client ne peut contenir que des lettres, des chiffres, des traits de soulignement et des tirets.",
|
"invalid_client_id": "L'ID client ne peut contenir que des lettres, des chiffres, des traits de soulignement et des tirets.",
|
||||||
"custom_client_id_description": "Définissez un identifiant client personnalisé si votre application l'exige. Sinon, laissez ce champ vide pour qu'un identifiant aléatoire soit généré.",
|
"custom_client_id_description": "Définissez un identifiant client personnalisé si votre application l'exige. Sinon, laissez ce champ vide pour qu'un identifiant aléatoire soit généré.",
|
||||||
"generated": "Généré",
|
"generated": "Généré",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Nom d'affichage",
|
"display_name": "Nom d'affichage",
|
||||||
"configure_application_images": "Configurer les images d'application",
|
"configure_application_images": "Configurer les images d'application",
|
||||||
"ui_config_disabled_info_title": "Configuration de l'interface utilisateur désactivée",
|
"ui_config_disabled_info_title": "Configuration de l'interface utilisateur désactivée",
|
||||||
"ui_config_disabled_info_description": "La configuration de l'interface utilisateur est désactivée parce que les paramètres de configuration de l'application sont gérés par des variables d'environnement. Certains paramètres peuvent ne pas être modifiables."
|
"ui_config_disabled_info_description": "La configuration de l'interface utilisateur est désactivée parce que les paramètres de configuration de l'application sont gérés par des variables d'environnement. Certains paramètres peuvent ne pas être modifiables.",
|
||||||
|
"logo_from_url_description": "Collez une URL pointant une image (svg, png, webp). Trouvez des icônes sur <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> ou <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "URL invalide",
|
||||||
|
"require_user_email": "Adresse e-mail requise",
|
||||||
|
"require_user_email_description": "Les utilisateurs doivent avoir une adresse e-mail. Si cette option est désactivée, ceux qui n'ont pas d'adresse e-mail ne pourront pas utiliser les fonctionnalités nécessitant un e-mail.",
|
||||||
|
"view": "Voir",
|
||||||
|
"toggle_columns": "Basculer les colonnes",
|
||||||
|
"locale": "Paramètres régionaux",
|
||||||
|
"ldap_id": "ID LDAP",
|
||||||
|
"reauthentication": "Réauthentification",
|
||||||
|
"clear_filters": "Effacer les filtres",
|
||||||
|
"default_profile_picture": "Photo de profil par défaut"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"use_your_passkey_instead": "Usare invece la tua passkey?",
|
"use_your_passkey_instead": "Usare invece la tua passkey?",
|
||||||
"email_login": "Accesso Email",
|
"email_login": "Accesso Email",
|
||||||
"enter_a_login_code_to_sign_in": "Inserisci un codice di accesso per accedere.",
|
"enter_a_login_code_to_sign_in": "Inserisci un codice di accesso per accedere.",
|
||||||
|
"sign_in_with_login_code": "Accedi con il codice di accesso",
|
||||||
"request_a_login_code_via_email": "Richiedi un codice di accesso via email.",
|
"request_a_login_code_via_email": "Richiedi un codice di accesso via email.",
|
||||||
"go_back": "Torna indietro",
|
"go_back": "Torna indietro",
|
||||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "È stata inviata un'email all'indirizzo fornito, se esiste nel sistema.",
|
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "È stata inviata un'email all'indirizzo fornito, se esiste nel sistema.",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"name_passkey": "Nome Passkey",
|
"name_passkey": "Nome Passkey",
|
||||||
"name_your_passkey_to_easily_identify_it_later": "Dai un nome alla tua passkey per identificarla facilmente in seguito.",
|
"name_your_passkey_to_easily_identify_it_later": "Dai un nome alla tua passkey per identificarla facilmente in seguito.",
|
||||||
"create_api_key": "Crea Chiave API",
|
"create_api_key": "Crea Chiave API",
|
||||||
"add_a_new_api_key_for_programmatic_access": "Aggiungi una nuova chiave API per l'accesso programmatico.",
|
"add_a_new_api_key_for_programmatic_access": "Aggiungi una nuova chiave API per accedere in modo programmatico <link href='https://pocket-id.org/docs/api'>all'API Pocket ID</link>.",
|
||||||
"add_api_key": "Aggiungi Chiave API",
|
"add_api_key": "Aggiungi Chiave API",
|
||||||
"manage_api_keys": "Gestisci Chiavi API",
|
"manage_api_keys": "Gestisci Chiavi API",
|
||||||
"api_key_created": "Chiave API Creata",
|
"api_key_created": "Chiave API Creata",
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Sei sicuro di voler revocare la chiave API \"{apiKeyName}\"? Questo comprometterà qualsiasi integrazione che utilizza questa chiave.",
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Sei sicuro di voler revocare la chiave API \"{apiKeyName}\"? Questo comprometterà qualsiasi integrazione che utilizza questa chiave.",
|
||||||
"last_used": "Ultimo utilizzo",
|
"last_used": "Ultimo utilizzo",
|
||||||
"actions": "Azioni",
|
"actions": "Azioni",
|
||||||
"images_updated_successfully": "Immagini aggiornate con successo",
|
"images_updated_successfully": "Immagini aggiornate senza problemi. Potrebbe volerci qualche minuto per l'aggiornamento.",
|
||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
"configure_smtp_to_send_emails": "Abilita le notifiche email per avvisare gli utenti quando viene rilevato un accesso da un nuovo dispositivo o posizione.",
|
"configure_smtp_to_send_emails": "Abilita le notifiche email per avvisare gli utenti quando viene rilevato un accesso da un nuovo dispositivo o posizione.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
"no_preview_data_available": "Dati di anteprima non disponibili",
|
"no_preview_data_available": "Dati di anteprima non disponibili",
|
||||||
"copy_all": "Copia tutto",
|
"copy_all": "Copia tutto",
|
||||||
"preview": "Anteprima",
|
"preview": "Anteprima",
|
||||||
"preview_for_user": "Anteprima per {name} ({email})",
|
"preview_for_user": "Anteprima per {name}",
|
||||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Anteprima dei dati OIDC che saranno inviati per l'utente",
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Anteprima dei dati OIDC che saranno inviati per l'utente",
|
||||||
"show": "Mostra",
|
"show": "Mostra",
|
||||||
"select_an_option": "Seleziona un'opzione",
|
"select_an_option": "Seleziona un'opzione",
|
||||||
@@ -450,5 +451,16 @@
|
|||||||
"display_name": "Nome visualizzato",
|
"display_name": "Nome visualizzato",
|
||||||
"configure_application_images": "Configurare le immagini dell'applicazione",
|
"configure_application_images": "Configurare le immagini dell'applicazione",
|
||||||
"ui_config_disabled_info_title": "Configurazione dell'interfaccia utente disattivata",
|
"ui_config_disabled_info_title": "Configurazione dell'interfaccia utente disattivata",
|
||||||
"ui_config_disabled_info_description": "La configurazione dell'interfaccia utente è disattivata perché le impostazioni di configurazione dell'applicazione sono gestite tramite variabili di ambiente. Alcune impostazioni potrebbero non essere modificabili."
|
"ui_config_disabled_info_description": "La configurazione dell'interfaccia utente è disattivata perché le impostazioni di configurazione dell'applicazione sono gestite tramite variabili di ambiente. Alcune impostazioni potrebbero non essere modificabili.",
|
||||||
|
"logo_from_url_description": "Incolla l'URL diretto dell'immagine (svg, png, webp). Trova le icone su <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> o <link href=\"https://dashboardicons.com\">Dashboard Icons</link>.",
|
||||||
|
"invalid_url": "URL non valido",
|
||||||
|
"require_user_email": "Richiesta indirizzo e-mail",
|
||||||
|
"require_user_email_description": "Chiede agli utenti di avere un indirizzo email. Se disattivato, chi non ha un indirizzo email non potrà usare le funzioni che lo richiedono.",
|
||||||
|
"view": "Visualizza",
|
||||||
|
"toggle_columns": "Attiva/disattiva colonne",
|
||||||
|
"locale": "Locale",
|
||||||
|
"ldap_id": "ID LDAP",
|
||||||
|
"reauthentication": "Riautenticazione",
|
||||||
|
"clear_filters": "Cancella filtri",
|
||||||
|
"default_profile_picture": "Immagine del profilo predefinita"
|
||||||
}
|
}
|
||||||
|
|||||||
466
frontend/messages/ja.json
Normal file
466
frontend/messages/ja.json
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"my_account": "マイアカウント",
|
||||||
|
"logout": "ログアウト",
|
||||||
|
"confirm": "確認",
|
||||||
|
"docs": "ドキュメント",
|
||||||
|
"key": "キー",
|
||||||
|
"value": "値",
|
||||||
|
"remove_custom_claim": "カスタムクレームを削除",
|
||||||
|
"add_custom_claim": "カスタムクレームを追加",
|
||||||
|
"add_another": "さらに追加",
|
||||||
|
"select_a_date": "日付を選択",
|
||||||
|
"select_file": "ファイルを選択",
|
||||||
|
"profile_picture": "プロフィール画像",
|
||||||
|
"profile_picture_is_managed_by_ldap_server": "プロフィール画像はLDAPサーバーによって管理されており、ここでは変更できません。",
|
||||||
|
"click_profile_picture_to_upload_custom": "プロフィール画像をクリックして、ファイルからカスタム画像をアップロードします。",
|
||||||
|
"image_should_be_in_format": "画像はPNGまたはJPEG形式である必要があります。",
|
||||||
|
"items_per_page": "ページあたりの表示件数",
|
||||||
|
"no_items_found": "項目が見つかりません",
|
||||||
|
"select_items": "項目を選択…",
|
||||||
|
"search": "検索…",
|
||||||
|
"expand_card": "カードを展開",
|
||||||
|
"copied": "コピーしました",
|
||||||
|
"click_to_copy": "クリックしてコピー",
|
||||||
|
"something_went_wrong": "エラーが発生しました",
|
||||||
|
"go_back_to_home": "ホームへ戻る",
|
||||||
|
"alternative_sign_in_methods": "別のサインイン方法",
|
||||||
|
"login_background": "ログインの背景",
|
||||||
|
"logo": "ロゴ",
|
||||||
|
"login_code": "ログインコード",
|
||||||
|
"create_a_login_code_to_sign_in_without_a_passkey_once": "ユーザーがパスキーなしで一度だけサインインできるログインコードを作成する。",
|
||||||
|
"one_hour": "1 時間",
|
||||||
|
"twelve_hours": "12 時間",
|
||||||
|
"one_day": "1 日",
|
||||||
|
"one_week": "1 週間",
|
||||||
|
"one_month": "1 ヶ月",
|
||||||
|
"expiration": "有効期限",
|
||||||
|
"generate_code": "コードの生成",
|
||||||
|
"name": "名前",
|
||||||
|
"browser_unsupported": "ブラウザ未対応",
|
||||||
|
"this_browser_does_not_support_passkeys": "このブラウザはパスキーをサポートしていません。別のサインイン方法を使用してください。",
|
||||||
|
"an_unknown_error_occurred": "不明なエラーが発生しました",
|
||||||
|
"authentication_process_was_aborted": "認証プロセスが中止されました",
|
||||||
|
"error_occurred_with_authenticator": "認証ツールでエラーが発生しました",
|
||||||
|
"authenticator_does_not_support_discoverable_credentials": "認証ツールは discoverable credential をサポートしていません",
|
||||||
|
"authenticator_does_not_support_resident_keys": "認証ツールは resident key をサポートしていません",
|
||||||
|
"passkey_was_previously_registered": "このパスキーは既に登録されています",
|
||||||
|
"authenticator_does_not_support_any_of_the_requested_algorithms": "認証ツールは要求されたアルゴリズムのいずれをもサポートしていません",
|
||||||
|
"authenticator_timed_out": "認証ツールがタイムアウトしました",
|
||||||
|
"critical_error_occurred_contact_administrator": "重大なエラーが発生しました。管理者にお問い合わせください。",
|
||||||
|
"sign_in_to": "{name} にサインイン",
|
||||||
|
"client_not_found": "クライアントが見つかりません",
|
||||||
|
"client_wants_to_access_the_following_information": "<b>{client}</b> は以下の情報にアクセスしようとしています:",
|
||||||
|
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "{appName} アカウントで <b>{client}</b> にサインインしますか?",
|
||||||
|
"email": "メール",
|
||||||
|
"view_your_email_address": "メールアドレスを表示",
|
||||||
|
"profile": "プロフィール",
|
||||||
|
"view_your_profile_information": "プロフィール情報を表示",
|
||||||
|
"groups": "グループ",
|
||||||
|
"view_the_groups_you_are_a_member_of": "あなたがメンバーであるグループを表示",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"sign_in": "サインイン",
|
||||||
|
"try_again": "リトライ",
|
||||||
|
"client_logo": "クライアントロゴ",
|
||||||
|
"sign_out": "サインアウト",
|
||||||
|
"do_you_want_to_sign_out_of_pocketid_with_the_account": "<b>{username}</b> アカウントで {appName} からサインアウトしますか?",
|
||||||
|
"sign_in_to_appname": "{appName} にサインイン",
|
||||||
|
"please_try_to_sign_in_again": "もう一度サインインしてください。",
|
||||||
|
"authenticate_with_passkey_to_access_account": "アカウントにアクセスするには、パスキーで認証してください。",
|
||||||
|
"authenticate": "認証",
|
||||||
|
"please_try_again": "再度お試しください。",
|
||||||
|
"continue": "続行",
|
||||||
|
"alternative_sign_in": "別のサインイン",
|
||||||
|
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "パスキーにアクセスできない場合は、以下のいずれかの方法でサインインできます。",
|
||||||
|
"use_your_passkey_instead": "代わりにパスキーを使用しますか?",
|
||||||
|
"email_login": "メールでログイン",
|
||||||
|
"enter_a_login_code_to_sign_in": "ログインコードを入力してサインインしてください。",
|
||||||
|
"sign_in_with_login_code": "ログインコードでサインイン",
|
||||||
|
"request_a_login_code_via_email": "メールでログインコードをリクエストする。",
|
||||||
|
"go_back": "戻る",
|
||||||
|
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "システムに存在する場合、指定されたメールアドレスにメールが送信されました。",
|
||||||
|
"enter_code": "コードを入力",
|
||||||
|
"enter_your_email_address_to_receive_an_email_with_a_login_code": "ログインコードのメールを受信するには、メールアドレスを入力してください。",
|
||||||
|
"your_email": "あなたのメール",
|
||||||
|
"submit": "送信",
|
||||||
|
"enter_the_code_you_received_to_sign_in": "サインインするために受け取ったコードを入力してください。",
|
||||||
|
"code": "コード",
|
||||||
|
"invalid_redirect_url": "無効な redirect URL",
|
||||||
|
"audit_log": "監査ログ",
|
||||||
|
"users": "ユーザー",
|
||||||
|
"user_groups": "ユーザーグループ",
|
||||||
|
"oidc_clients": "OIDCクライアント",
|
||||||
|
"api_keys": "API キー",
|
||||||
|
"application_configuration": "アプリケーション設定",
|
||||||
|
"settings": "設定",
|
||||||
|
"update_pocket_id": "Pocket ID を更新",
|
||||||
|
"powered_by": "Powered by",
|
||||||
|
"see_your_account_activities_from_the_last_3_months": "過去3ヶ月間のアカウントアクティビティを確認する。",
|
||||||
|
"time": "時間",
|
||||||
|
"event": "イベント",
|
||||||
|
"approximate_location": "おおよその場所",
|
||||||
|
"ip_address": "IPアドレス",
|
||||||
|
"device": "デバイス",
|
||||||
|
"client": "クライアント",
|
||||||
|
"unknown": "不明",
|
||||||
|
"account_details_updated_successfully": "アカウントの詳細が正常に更新されました",
|
||||||
|
"profile_picture_updated_successfully": "プロフィール画像が正常に更新されました。反映まで数分かかる場合があります。",
|
||||||
|
"account_settings": "アカウント設定",
|
||||||
|
"passkey_missing": "パスキーが見つかりません",
|
||||||
|
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "アカウントへのアクセスを失わないように、パスキーを追加してください。",
|
||||||
|
"single_passkey_configured": "Single Passkey Configured",
|
||||||
|
"it_is_recommended_to_add_more_than_one_passkey": "アカウントへのアクセスを失わないように、複数のパスキーを追加することをお勧めします。",
|
||||||
|
"account_details": "アカウントの詳細",
|
||||||
|
"passkeys": "パスキー",
|
||||||
|
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "認証に使用するパスキーを管理します。",
|
||||||
|
"add_passkey": "パスキーを追加",
|
||||||
|
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "パスキーなしで別のデバイスからサインインするためのワンタイムログインコードを作成します。",
|
||||||
|
"create": "作成",
|
||||||
|
"first_name": "名",
|
||||||
|
"last_name": "姓",
|
||||||
|
"username": "ユーザー名",
|
||||||
|
"save": "保存",
|
||||||
|
"username_can_only_contain": "ユーザー名には小文字、数字、アンダー スコア、ドット、ハイフン、および '@' 記号のみを含めることができます",
|
||||||
|
"username_must_start_with": "ユーザー名は英数字で始まる必要があります",
|
||||||
|
"username_must_end_with": "ユーザー名は英数字で終わる必要があります",
|
||||||
|
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "以下のコードを使用してサインインしてください。コードは15分後に失効します。",
|
||||||
|
"or_visit": "or visit",
|
||||||
|
"added_on": "追加日",
|
||||||
|
"rename": "名前を変更",
|
||||||
|
"delete": "削除",
|
||||||
|
"are_you_sure_you_want_to_delete_this_passkey": "このパスキーを削除してもよろしいですか?",
|
||||||
|
"passkey_deleted_successfully": "パスキーが正常に削除されました",
|
||||||
|
"delete_passkey_name": "{passkeyName} を削除",
|
||||||
|
"passkey_name_updated_successfully": "パスキー名が正常に更新されました",
|
||||||
|
"name_passkey": "パスキー名",
|
||||||
|
"name_your_passkey_to_easily_identify_it_later": "パスキーを後で簡単に識別するように名前を付けます。",
|
||||||
|
"create_api_key": "API キーを作成",
|
||||||
|
"add_a_new_api_key_for_programmatic_access": "<link href='https://pocket-id.org/docs/api'>Pocket ID API</link> へのプログラムによるアクセス用に新しいAPI キーを追加します。",
|
||||||
|
"add_api_key": "API キーを追加",
|
||||||
|
"manage_api_keys": "API キーの管理",
|
||||||
|
"api_key_created": "APIキー が作成されました",
|
||||||
|
"for_security_reasons_this_key_will_only_be_shown_once": "セキュリティ上の理由から、このキーは一度だけ表示されます。安全に保管してください。",
|
||||||
|
"description": "説明",
|
||||||
|
"api_key": "API キー",
|
||||||
|
"close": "閉じる",
|
||||||
|
"name_to_identify_this_api_key": "この API キーを識別するための名前。",
|
||||||
|
"expires_at": "有効期限",
|
||||||
|
"when_this_api_key_will_expire": "このAPI キーが期限切れになる日付。",
|
||||||
|
"optional_description_to_help_identify_this_keys_purpose": "このキーの目的を特定するのに役立つ任意の説明。",
|
||||||
|
"expiration_date_must_be_in_the_future": "有効期限は未来の日付にする必要があります",
|
||||||
|
"revoke_api_key": "API キーの削除",
|
||||||
|
"never": "なし",
|
||||||
|
"revoke": "削除",
|
||||||
|
"api_key_revoked_successfully": "API キーが正常に削除されました",
|
||||||
|
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "APIキー \"{apiKeyName}\" を本当に削除しますか?このAPI キーを使用しているすべての連携が停止します。",
|
||||||
|
"last_used": "最終使用日",
|
||||||
|
"actions": "Actions",
|
||||||
|
"images_updated_successfully": "画像の更新が正常に完了しました。更新には数分かかる場合があります。",
|
||||||
|
"general": "一般",
|
||||||
|
"configure_smtp_to_send_emails": "新しいデバイスや場所からのログインが検出された際にユーザーに警告するメール通知を有効にします。",
|
||||||
|
"ldap": "LDAP",
|
||||||
|
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "LDAP サーバーからユーザーとグループを同期するように LDAP 設定を構成します。",
|
||||||
|
"images": "画像",
|
||||||
|
"update": "更新",
|
||||||
|
"email_configuration_updated_successfully": "メール設定が正常に更新されました",
|
||||||
|
"save_changes_question": "変更を保存しますか?",
|
||||||
|
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "テストメールを送信する前に変更を保存する必要があります。今すぐ保存しますか?",
|
||||||
|
"save_and_send": "保存して送信",
|
||||||
|
"test_email_sent_successfully": "テストメールがあなたのメールアドレスに正常に送信されました。",
|
||||||
|
"failed_to_send_test_email": "テストメールの送信に失敗しました。詳細についてはサーバーのログを確認してください。",
|
||||||
|
"smtp_configuration": "SMTP 設定",
|
||||||
|
"smtp_host": "SMTP ホスト",
|
||||||
|
"smtp_port": "SMTP ポート",
|
||||||
|
"smtp_user": "SMTP ユーザー",
|
||||||
|
"smtp_password": "SMTP パスワード",
|
||||||
|
"smtp_from": "SMTP 送信元",
|
||||||
|
"smtp_tls_option": "SMTP TLS オプション",
|
||||||
|
"email_tls_option": "メール TLS オプション",
|
||||||
|
"skip_certificate_verification": "証明書検証をスキップ",
|
||||||
|
"this_can_be_useful_for_selfsigned_certificates": "これは自己署名証明書の場合に有用です。",
|
||||||
|
"enabled_emails": "有効なメール",
|
||||||
|
"email_login_notification": "メールログイン通知",
|
||||||
|
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "ユーザーが新しいデバイスからログインした際にメールを送信する。",
|
||||||
|
"emai_login_code_requested_by_user": "ユーザーがリクエストしたメールログインコード",
|
||||||
|
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "ユーザーがメールに送信されたログインコードをリクエストすることでパスキーをバイパスできるようにします。これによりセキュリティが大幅に低下し、ユーザーのメールにアクセスできる者なら誰でもログイン可能になります。",
|
||||||
|
"email_login_code_from_admin": "管理者からのメールログインコード",
|
||||||
|
"allows_an_admin_to_send_a_login_code_to_the_user": "管理者がメールでログインコードをユーザーに送信することを許可します。",
|
||||||
|
"send_test_email": "テストメールを送信",
|
||||||
|
"application_configuration_updated_successfully": "アプリケーション設定が正常に更新されました",
|
||||||
|
"application_name": "アプリケーション名",
|
||||||
|
"session_duration": "セッション期間",
|
||||||
|
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "ユーザーが再度ログインする必要があるまでのセッションの継続時間。(分単位)",
|
||||||
|
"enable_self_account_editing": "自身のアカウント編集を有効にする",
|
||||||
|
"whether_the_users_should_be_able_to_edit_their_own_account_details": "ユーザーが自身のアカウントの詳細を編集できるかどうか。",
|
||||||
|
"emails_verified": "メールアドレス確認済み",
|
||||||
|
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "ユーザのEメールをOIDCクライアントで検証済みとしてマークするかどうか。",
|
||||||
|
"ldap_configuration_updated_successfully": "LDAP 設定が正常に更新されました",
|
||||||
|
"ldap_disabled_successfully": "LDAPは正常に無効化されました",
|
||||||
|
"ldap_sync_finished": "LDAP同期が完了しました",
|
||||||
|
"client_configuration": "クライアントの設定",
|
||||||
|
"ldap_url": "LDAP URL",
|
||||||
|
"ldap_bind_dn": "LDAP Bind DN",
|
||||||
|
"ldap_bind_password": "LDAP Bind Password",
|
||||||
|
"ldap_base_dn": "LDAP Base DN",
|
||||||
|
"user_search_filter": "ユーザー検索フィルター",
|
||||||
|
"the_search_filter_to_use_to_search_or_sync_users": "ユーザーの検索、同期に使用する検索フィルター。",
|
||||||
|
"groups_search_filter": "グループ検索フィルター",
|
||||||
|
"the_search_filter_to_use_to_search_or_sync_groups": "グループの検索、同期に使用する検索フィルター。",
|
||||||
|
"attribute_mapping": "属性マッピング",
|
||||||
|
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||||
|
"the_value_of_this_attribute_should_never_change": "この属性の値は決して変更されてはなりません。",
|
||||||
|
"username_attribute": "ユーザー名属性",
|
||||||
|
"user_mail_attribute": "ユーザーメール属性",
|
||||||
|
"user_first_name_attribute": "ユーザーの名属性",
|
||||||
|
"user_last_name_attribute": "ユーザーの姓属性",
|
||||||
|
"user_profile_picture_attribute": "ユーザープロフィール画像属性",
|
||||||
|
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "この属性の値は、URL、バイナリ、またはBase64エンコードされた画像のいずれかです。",
|
||||||
|
"group_members_attribute": "グループメンバー属性",
|
||||||
|
"the_attribute_to_use_for_querying_members_of_a_group": "グループのメンバーをクエリする際に使用する属性。",
|
||||||
|
"group_unique_identifier_attribute": "グループ固有識別子属性",
|
||||||
|
"group_rdn_attribute": "Group RDN Attribute (in DN)",
|
||||||
|
"admin_group_name": "管理者グループ名",
|
||||||
|
"members_of_this_group_will_have_admin_privileges_in_pocketid": "このグループのメンバーはPocket IDで管理者権限を持っています。",
|
||||||
|
"disable": "無効",
|
||||||
|
"sync_now": "今すぐ同期",
|
||||||
|
"enable": "有効",
|
||||||
|
"user_created_successfully": "ユーザーは正常に作成されました",
|
||||||
|
"create_user": "ユーザーを作成",
|
||||||
|
"add_a_new_user_to_appname": "{appName} に新しいユーザーを追加",
|
||||||
|
"add_user": "ユーザーを追加",
|
||||||
|
"manage_users": "ユーザー管理",
|
||||||
|
"admin_privileges": "管理者権限",
|
||||||
|
"admins_have_full_access_to_the_admin_panel": "管理者は管理パネルへのフルアクセス権限を持っています。",
|
||||||
|
"delete_firstname_lastname": "削除 {firstName} {lastName}",
|
||||||
|
"are_you_sure_you_want_to_delete_this_user": "このユーザーを削除してもよろしいですか?",
|
||||||
|
"user_deleted_successfully": "ユーザーの削除が正常に完了しました",
|
||||||
|
"role": "ロール",
|
||||||
|
"source": "Source",
|
||||||
|
"admin": "管理者",
|
||||||
|
"user": "ユーザー",
|
||||||
|
"local": "ローカル",
|
||||||
|
"toggle_menu": "Toggle menu",
|
||||||
|
"edit": "編集",
|
||||||
|
"user_groups_updated_successfully": "ユーザーグループが正常に更新されました",
|
||||||
|
"user_updated_successfully": "ユーザーは正常に更新されました",
|
||||||
|
"custom_claims_updated_successfully": "カスタムクレームが正常に更新されました",
|
||||||
|
"back": "Back",
|
||||||
|
"user_details_firstname_lastname": "ユーザーの詳細 {firstName} {lastName}",
|
||||||
|
"manage_which_groups_this_user_belongs_to": "このユーザーが所属するグループを管理します。",
|
||||||
|
"custom_claims": "カスタムクレーム",
|
||||||
|
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。",
|
||||||
|
"user_group_created_successfully": "ユーザーグループが正常に作成されました",
|
||||||
|
"create_user_group": "ユーザーグループを作成",
|
||||||
|
"create_a_new_group_that_can_be_assigned_to_users": "ユーザーに割り当て可能な新しいグループを作成します。",
|
||||||
|
"add_group": "グループを追加",
|
||||||
|
"manage_user_groups": "ユーザーグループの管理",
|
||||||
|
"friendly_name": "Friendly Name",
|
||||||
|
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
|
||||||
|
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||||
|
"delete_name": "{name} を削除",
|
||||||
|
"are_you_sure_you_want_to_delete_this_user_group": "このユーザーグループを削除しますか?",
|
||||||
|
"user_group_deleted_successfully": "ユーザーグループが正常に削除されました",
|
||||||
|
"user_count": "ユーザー数",
|
||||||
|
"user_group_updated_successfully": "ユーザーグループが正常に更新されました",
|
||||||
|
"users_updated_successfully": "ユーザーは正常に更新されました",
|
||||||
|
"user_group_details_name": "ユーザーグループの詳細 {name}",
|
||||||
|
"assign_users_to_this_group": "このグループにユーザーを割り当てます。",
|
||||||
|
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "カスタムクレームは、ユーザーに関する追加情報を保存するために使用できるキーと値のペアです。これらのクレームは、'profile' スコープが要求された場合に ID トークンに含まれます。競合が発生した場合、ユーザーに定義されたカスタムクレームが優先されます。",
|
||||||
|
"oidc_client_created_successfully": "OIDC クライアントが正常に作成されました",
|
||||||
|
"create_oidc_client": "OIDC クライアントの作成",
|
||||||
|
"add_a_new_oidc_client_to_appname": "{appName} に新しい OIDC クライアントを追加します。",
|
||||||
|
"add_oidc_client": "OIDC クライアントを追加",
|
||||||
|
"manage_oidc_clients": "OIDC クライアントの管理",
|
||||||
|
"one_time_link": "ワンタイムリンク",
|
||||||
|
"use_this_link_to_sign_in_once": "このリンクを使用してサインインしてください。これは、まだパスキーを追加していないユーザーや、パスキーを紛失したユーザーに必要な手順です。",
|
||||||
|
"add": "追加",
|
||||||
|
"callback_urls": "Callback URLs",
|
||||||
|
"logout_callback_urls": "Logout Callback URLs",
|
||||||
|
"public_client": "Public Client",
|
||||||
|
"public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
|
||||||
|
"pkce": "PKCE",
|
||||||
|
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||||
|
"requires_reauthentication": "再認証が必要",
|
||||||
|
"requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in",
|
||||||
|
"name_logo": "{name} ロゴ",
|
||||||
|
"change_logo": "ロゴの変更",
|
||||||
|
"upload_logo": "ロゴをアップロード",
|
||||||
|
"remove_logo": "ロゴを削除",
|
||||||
|
"are_you_sure_you_want_to_delete_this_oidc_client": "このOIDCクライアントを削除してもよろしいですか?",
|
||||||
|
"oidc_client_deleted_successfully": "OIDC クライアントが正常に削除されました",
|
||||||
|
"authorization_url": "Authorization URL",
|
||||||
|
"oidc_discovery_url": "OIDC Discovery URL",
|
||||||
|
"token_url": "Token URL",
|
||||||
|
"userinfo_url": "Userinfo URL",
|
||||||
|
"logout_url": "Logout URL",
|
||||||
|
"certificate_url": "Certificate URL",
|
||||||
|
"enabled": "有効",
|
||||||
|
"disabled": "無効",
|
||||||
|
"oidc_client_updated_successfully": "OIDC クライアントが正常に更新されました",
|
||||||
|
"create_new_client_secret": "新しいクライアントシークレットを作成する",
|
||||||
|
"are_you_sure_you_want_to_create_a_new_client_secret": "新しいクライアントシークレットを作成してもよろしいですか?古いシークレットは無効化されます。",
|
||||||
|
"generate": "Generate",
|
||||||
|
"new_client_secret_created_successfully": "新しいクライアントシークレットが正常に作成されました",
|
||||||
|
"allowed_user_groups_updated_successfully": "許可されたユーザーグループが正常に更新されました",
|
||||||
|
"oidc_client_name": "OIDC クライアント {name}",
|
||||||
|
"client_id": "Client ID",
|
||||||
|
"client_secret": "クライアントシークレット",
|
||||||
|
"show_more_details": "詳細を表示",
|
||||||
|
"allowed_user_groups": "許可されたユーザーグループ",
|
||||||
|
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
||||||
|
"favicon": "Favicon",
|
||||||
|
"light_mode_logo": "ライトモードのロゴ",
|
||||||
|
"dark_mode_logo": "ダークモードのロゴ",
|
||||||
|
"background_image": "背景画像",
|
||||||
|
"language": "言語",
|
||||||
|
"reset_profile_picture_question": "プロフィール画像をリセットしますか?",
|
||||||
|
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "アップロードされた画像を削除し、プロフィール画像をデフォルトにリセットします。続行しますか?",
|
||||||
|
"reset": "リセット",
|
||||||
|
"reset_to_default": "デフォルトに戻す",
|
||||||
|
"profile_picture_has_been_reset": "プロフィール画像がリセットされました。更新には数分かかる場合があります。",
|
||||||
|
"select_the_language_you_want_to_use": "使用する言語を選択してください。一部のテキストは自動翻訳により、不正確な場合がありますのでご注意ください。",
|
||||||
|
"contribute_to_translation": "問題が見つかった場合は、 <link href='https://crowdin.com/project/pocket-id'>Crowdin</link> で翻訳に貢献してください。",
|
||||||
|
"personal": "Personal",
|
||||||
|
"global": "Global",
|
||||||
|
"all_users": "すべてのユーザー",
|
||||||
|
"all_events": "すべてのイベント",
|
||||||
|
"all_clients": "すべてのクライアント",
|
||||||
|
"all_locations": "すべての場所",
|
||||||
|
"global_audit_log": "グローバル監査ログ",
|
||||||
|
"see_all_account_activities_from_the_last_3_months": "過去3ヶ月間のすべてのユーザーアクティビティを表示します。",
|
||||||
|
"token_sign_in": "トークンサインイン",
|
||||||
|
"client_authorization": "Client Authorization",
|
||||||
|
"new_client_authorization": "New Client Authorization",
|
||||||
|
"disable_animations": "アニメーションの無効化",
|
||||||
|
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||||
|
"user_disabled": "アカウントの無効化",
|
||||||
|
"disabled_users_cannot_log_in_or_use_services": "無効化されたユーザーはログインやサービスを利用できません。",
|
||||||
|
"user_disabled_successfully": "ユーザーが正常に無効化されました。",
|
||||||
|
"user_enabled_successfully": "ユーザーが正常に有効化されました。",
|
||||||
|
"status": "Status",
|
||||||
|
"disable_firstname_lastname": "無効化 {firstName} {lastName}",
|
||||||
|
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||||
|
"ldap_soft_delete_users": "LDAPから無効なユーザーを保持します。",
|
||||||
|
"ldap_soft_delete_users_description": "有効にすると、LDAPから削除されたユーザーはシステムから削除されるのではなく、無効化されます。",
|
||||||
|
"login_code_email_success": "ログインコードがユーザーに送信されました。",
|
||||||
|
"send_email": "メールを送信",
|
||||||
|
"show_code": "コードを表示",
|
||||||
|
"callback_url_description": "クライアントが提供するURL。空白のままにすると自動的に追加されます。ワイルドカード(*)はサポートされていますが、セキュリティを向上させるためには避けてください。",
|
||||||
|
"logout_callback_url_description": "クライアントがログアウト用に提供するURL。ワイルドカード(*)はサポートされていますが、セキュリティを向上させるためには避けてください。",
|
||||||
|
"api_key_expiration": "API キーの有効期限",
|
||||||
|
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "API キーの有効期限が近づいたら、ユーザーにメールを送信します。",
|
||||||
|
"authorize_device": "デバイスの認証",
|
||||||
|
"the_device_has_been_authorized": "デバイスは認証されました。",
|
||||||
|
"enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。",
|
||||||
|
"authorize": "Authorize",
|
||||||
|
"federated_client_credentials": "連携クライアントの資格情報",
|
||||||
|
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
|
||||||
|
"add_federated_client_credential": "Add Federated Client Credential",
|
||||||
|
"add_another_federated_client_credential": "Add another federated client credential",
|
||||||
|
"oidc_allowed_group_count": "許可されたグループ数",
|
||||||
|
"unrestricted": "制限なし",
|
||||||
|
"show_advanced_options": "詳細設定を表示",
|
||||||
|
"hide_advanced_options": "詳細設定を隠す",
|
||||||
|
"oidc_data_preview": "OIDC Data Preview",
|
||||||
|
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||||
|
"id_token": "ID Token",
|
||||||
|
"access_token": "Access Token",
|
||||||
|
"userinfo": "Userinfo",
|
||||||
|
"id_token_payload": "ID Token Payload",
|
||||||
|
"access_token_payload": "Access Token Payload",
|
||||||
|
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||||
|
"copy": "コピー",
|
||||||
|
"no_preview_data_available": "No preview data available",
|
||||||
|
"copy_all": "すべてコピー",
|
||||||
|
"preview": "プレビュー",
|
||||||
|
"preview_for_user": "{name} のプレビュー",
|
||||||
|
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||||
|
"show": "表示",
|
||||||
|
"select_an_option": "Select an option",
|
||||||
|
"select_user": "ユーザーを選択",
|
||||||
|
"error": "エラー",
|
||||||
|
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "アクセントカラーを選択して、Pocket IDの外観をカスタマイズしてください。",
|
||||||
|
"accent_color": "アクセントカラー",
|
||||||
|
"custom_accent_color": "カスタムアクセントカラー",
|
||||||
|
"custom_accent_color_description": "有効な CSS カラーフォーマット (例: hex, rgb, hsl) を使用してカスタムカラーを入力します。",
|
||||||
|
"color_value": "Color Value",
|
||||||
|
"apply": "適用",
|
||||||
|
"signup_token": "サインアップトークン",
|
||||||
|
"create_a_signup_token_to_allow_new_user_registration": "新規ユーザー登録を許可するためのサインアップトークンを作成する。",
|
||||||
|
"usage_limit": "使用回数制限",
|
||||||
|
"number_of_times_token_can_be_used": "サインアップトークンが使用できる回数。",
|
||||||
|
"expires": "Expires",
|
||||||
|
"signup": "サインアップ",
|
||||||
|
"user_creation": "ユーザー作成",
|
||||||
|
"configure_user_creation": "新しいユーザーのサインアップ方法やデフォルトの権限など、ユーザー作成設定を管理します。",
|
||||||
|
"user_creation_groups_description": "新しいユーザーにこれらのグループを自動的に割り当てます。",
|
||||||
|
"user_creation_claims_description": "新しいユーザーにこれらのカスタムクレームを自動的に割り当てます。",
|
||||||
|
"user_creation_updated_successfully": "ユーザー作成設定が正常に更新されました。",
|
||||||
|
"signup_disabled_description": "ユーザーのサインアップは無効化されています。管理者のみが新しいユーザーアカウントを作成できます。",
|
||||||
|
"signup_requires_valid_token": "アカウントを作成するには有効なサインアップトークンが必要です",
|
||||||
|
"validating_signup_token": "サインアップトークンを検証中",
|
||||||
|
"go_to_login": "ログインへ移動",
|
||||||
|
"signup_to_appname": "{appName} にサインアップ",
|
||||||
|
"create_your_account_to_get_started": "アカウントを作成して始めましょう。",
|
||||||
|
"initial_account_creation_description": "ご利用を開始するにはアカウントを作成してください。パスキーの設定は後で可能です。",
|
||||||
|
"setup_your_passkey": "パスキーを設定",
|
||||||
|
"create_a_passkey_to_securely_access_your_account": "安全にアカウントにアクセスするためのパスキーを作成します。これがサインインの主な方法になります。",
|
||||||
|
"skip_for_now": "あとで",
|
||||||
|
"account_created": "アカウントが作成されました",
|
||||||
|
"enable_user_signups": "ユーザーサインアップを有効にする",
|
||||||
|
"enable_user_signups_description": "Pocket IDでユーザーが新規アカウントを登録する方法を決定します。",
|
||||||
|
"user_signups_are_disabled": "ユーザーのサインアップは現在無効化されています",
|
||||||
|
"create_signup_token": "サインアップトークンを作成",
|
||||||
|
"view_active_signup_tokens": "有効なサインアップトークンを表示",
|
||||||
|
"manage_signup_tokens": "サインアップトークンの管理",
|
||||||
|
"view_and_manage_active_signup_tokens": "有効なサインアップトークンの表示と管理。",
|
||||||
|
"signup_token_deleted_successfully": "サインアップトークンが正常に削除されました。",
|
||||||
|
"expired": "Expired",
|
||||||
|
"used_up": "Used Up",
|
||||||
|
"active": "Active",
|
||||||
|
"usage": "Usage",
|
||||||
|
"created": "Created",
|
||||||
|
"token": "トークン",
|
||||||
|
"loading": "読み込み中",
|
||||||
|
"delete_signup_token": "サインアップトークンを削除",
|
||||||
|
"are_you_sure_you_want_to_delete_this_signup_token": "このサインアップトークンを削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
|
"signup_with_token": "トークンでサインアップ",
|
||||||
|
"signup_with_token_description": "ユーザーは管理者が作成した有効なサインアップトークンを使用してのみサインアップできます。",
|
||||||
|
"signup_open": "サインアップを開く",
|
||||||
|
"signup_open_description": "誰でも制限なしに新しいアカウントを作成できます。",
|
||||||
|
"of": "of",
|
||||||
|
"skip_passkey_setup": "パスキーの設定をスキップ",
|
||||||
|
"skip_passkey_setup_description": "パスキーの設定を強く推奨します。設定していない場合、セッションが切れた時点でアカウントにアクセスできなくなります。",
|
||||||
|
"my_apps": "マイアプリ",
|
||||||
|
"no_apps_available": "利用可能なアプリはありません",
|
||||||
|
"contact_your_administrator_for_app_access": "アプリケーションにアクセスするには、管理者に問い合わせてください。",
|
||||||
|
"launch": "起動",
|
||||||
|
"client_launch_url": "Client Launch URL",
|
||||||
|
"client_launch_url_description": "ユーザーが「マイアプリ」ページからアプリを起動した際に開かれるURL。",
|
||||||
|
"client_name_description": "The name of the client that shows in the Pocket ID UI.",
|
||||||
|
"revoke_access": "アクセスを取り消す",
|
||||||
|
"revoke_access_description": "<b>{clientName}</b>へのアクセスを取り消します。 <b>{clientName}</b> はあなたのアカウント情報にアクセスできなくなります。",
|
||||||
|
"revoke_access_successful": "{clientName} へのアクセスは正常に取り消されました。",
|
||||||
|
"last_signed_in_ago": "Last signed in {time} ago",
|
||||||
|
"invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||||
|
"generated": "Generated",
|
||||||
|
"administration": "Administration",
|
||||||
|
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
|
||||||
|
"display_name_attribute": "Display Name Attribute",
|
||||||
|
"display_name": "表示名",
|
||||||
|
"configure_application_images": "アプリケーションの画像を設定",
|
||||||
|
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||||
|
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.",
|
||||||
|
"logo_from_url_description": "画像の直接のURL (svg, png, webp) を貼り付けます。アイコンは <link href=\"https://selfh.st/icons\">Selfh.st Icons</link> か <link href=\"https://dashboardicons.com\">Dashboard Icons</link> で探せます。",
|
||||||
|
"invalid_url": "無効な URL",
|
||||||
|
"require_user_email": "Require Email Address",
|
||||||
|
"require_user_email_description": "ユーザーにメールアドレスの登録を必須とします。無効にした場合、メールアドレスを持たないユーザーはメールアドレスが必要な機能を利用できなくなります。",
|
||||||
|
"view": "表示",
|
||||||
|
"toggle_columns": "列の表示/非表示を切り替える",
|
||||||
|
"locale": "ロケール",
|
||||||
|
"ldap_id": "LDAP ID",
|
||||||
|
"reauthentication": "再認証",
|
||||||
|
"clear_filters": "フィルターをクリア",
|
||||||
|
"default_profile_picture": "デフォルトのプロフィール画像"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user