mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-28 18:26:36 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96303ded2b | ||
|
|
d06257ec9b | ||
|
|
19ef4833e9 | ||
|
|
e2c38138be | ||
|
|
13b02a072f | ||
|
|
430421e98b | ||
|
|
61e71ad43b | ||
|
|
4db44e4818 | ||
|
|
9ab178712a | ||
|
|
ecd74b794f | ||
|
|
5afd651434 | ||
|
|
2d3cba6308 | ||
|
|
e607fe424a | ||
|
|
8ae446322a | ||
|
|
37a835b44e | ||
|
|
75f531fbc6 | ||
|
|
28346da731 | ||
|
|
a1b20f0e74 | ||
|
|
7497f4ad40 | ||
|
|
b530d646ac | ||
|
|
77985800ae | ||
|
|
ea21eba281 | ||
|
|
66edb18f2c | ||
|
|
dab37c5967 | ||
|
|
781ff7ae7b | ||
|
|
04c7f180de | ||
|
|
5c452ceef0 | ||
|
|
8cd834a503 | ||
|
|
a65ce56b42 | ||
|
|
4a97986f52 | ||
|
|
a879bfa418 | ||
|
|
164ce6a3d7 | ||
|
|
ef1aeb7152 | ||
|
|
47c39f6d38 | ||
|
|
2884021055 | ||
|
|
def39b8703 | ||
|
|
d071641890 | ||
|
|
397544c0f3 | ||
|
|
1fb99e5d52 | ||
|
|
7b403552ba |
21
.github/ISSUE_TEMPLATE/bug.yml
vendored
21
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -34,4 +34,23 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Before submitting, please check if the issues hasn't been raised before.
|
### Additional Information
|
||||||
|
- type: textarea
|
||||||
|
id: extra-information
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "Version and Environment"
|
||||||
|
description: "Please specify the version of Pocket ID, along with any environment-specific configurations, such your reverse proxy, that might be relevant."
|
||||||
|
placeholder: "e.g., v0.24.1"
|
||||||
|
- type: textarea
|
||||||
|
id: log-files
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "Log Output"
|
||||||
|
description: "Output of log files when the issue occured to help us diagnose the issue."
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**Before submitting, please check if the issue hasn't been raised before.**
|
||||||
|
|||||||
51
.github/workflows/deploy-docs.yml
vendored
Normal file
51
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Deploy Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Docusaurus
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: docs/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: ./docs
|
||||||
|
|
||||||
|
- name: Build website
|
||||||
|
run: npm run build
|
||||||
|
working-directory: ./docs
|
||||||
|
|
||||||
|
- name: Upload Build Artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: docs/build
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to GitHub Pages
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
9
.github/workflows/e2e-tests.yml
vendored
9
.github/workflows/e2e-tests.yml
vendored
@@ -2,8 +2,17 @@ name: E2E Tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "**.md"
|
||||||
|
- ".github/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "**.md"
|
||||||
|
- ".github/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|||||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,3 +1,42 @@
|
|||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.28.0...v) (2025-02-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't return error page if version info fetching failed ([d06257e](https://github.com/stonith404/pocket-id/commit/d06257ec9b5e46e25e40c174b4bef02dca0a1ea3))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.27.2...v) (2025-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow LDAP users and groups to be deleted if LDAP gets disabled ([9ab1787](https://github.com/stonith404/pocket-id/commit/9ab178712aa3cc71546a89226e67b7ba91245251))
|
||||||
|
* map allowed groups to OIDC clients ([#202](https://github.com/stonith404/pocket-id/issues/202)) ([13b02a0](https://github.com/stonith404/pocket-id/commit/13b02a072f20ce10e12fd8b897cbf42a908f3291))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **caddy:** trusted_proxies for IPv6 enabled hosts ([#189](https://github.com/stonith404/pocket-id/issues/189)) ([37a835b](https://github.com/stonith404/pocket-id/commit/37a835b44e308622f6862de494738dd2bfb58ef0))
|
||||||
|
* missing user service dependency ([61e71ad](https://github.com/stonith404/pocket-id/commit/61e71ad43b8f0f498133d3eb2381382e7bc642b9))
|
||||||
|
* non LDAP user group can't be updated after update ([ecd74b7](https://github.com/stonith404/pocket-id/commit/ecd74b794f1ffb7da05bce0046fb8d096b039409))
|
||||||
|
* use cursor pointer on clickable elements ([7798580](https://github.com/stonith404/pocket-id/commit/77985800ae9628104e03e7f2e803b7ed9eaaf4e0))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.27.1...v) (2025-01-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* smtp hello for tls connections ([#180](https://github.com/stonith404/pocket-id/issues/180)) ([781ff7a](https://github.com/stonith404/pocket-id/commit/781ff7ae7b84b13892e7a565b7a78f20c52ee2c9))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.27.0...v) (2025-01-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add `__HOST` prefix to cookies ([#175](https://github.com/stonith404/pocket-id/issues/175)) ([164ce6a](https://github.com/stonith404/pocket-id/commit/164ce6a3d7fa8ae5275c94302952cf318e3b3113))
|
||||||
|
* send hostname derived from `PUBLIC_APP_URL` with SMTP EHLO command ([397544c](https://github.com/stonith404/pocket-id/commit/397544c0f3f2b49f1f34ae53e6b9daf194d1ae28))
|
||||||
|
* use OS hostname for SMTP EHLO message ([47c39f6](https://github.com/stonith404/pocket-id/commit/47c39f6d382c496cb964262adcf76cc8dbb96da3))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.26.0...v) (2025-01-22)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.26.0...v) (2025-01-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,19 +55,19 @@ The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in Ty
|
|||||||
3. Install the dependencies with `npm install`
|
3. Install the dependencies with `npm install`
|
||||||
4. Start the frontend with `npm run dev`
|
4. Start the frontend with `npm run dev`
|
||||||
|
|
||||||
You're all set!
|
|
||||||
|
|
||||||
### Reverse Proxy
|
### Reverse Proxy
|
||||||
We use [Caddy](https://caddyserver.com) as a reverse proxy. You can use any other reverse proxy if you want but you have to configure it yourself.
|
We use [Caddy](https://caddyserver.com) as a reverse proxy. You can use any other reverse proxy if you want but you have to configure it yourself.
|
||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
Run `caddy run --config reverse-proxy/Caddyfile` in the root folder.
|
Run `caddy run --config reverse-proxy/Caddyfile` in the root folder.
|
||||||
|
|
||||||
|
You're all set!
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
We are using [Playwright](https://playwright.dev) for end-to-end testing.
|
We are using [Playwright](https://playwright.dev) for end-to-end testing.
|
||||||
|
|
||||||
The tests can be run like this:
|
The tests can be run like this:
|
||||||
1. Start the backend normally
|
1. Start the backend normally
|
||||||
2. Start the frontend in production mode with `npm run build && node build/index.js`
|
2. Start the frontend in production mode with `npm run build && node --env-file=.env build/index.js`
|
||||||
3. Run the tests with `npm run test`
|
3. Run the tests with `npm run test`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Build Frontend
|
# Stage 1: Build Frontend
|
||||||
FROM node:20-alpine AS frontend-builder
|
FROM node:22-alpine AS frontend-builder
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
COPY ./frontend/package*.json ./
|
COPY ./frontend/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
@@ -20,7 +20,7 @@ WORKDIR /app/backend/cmd
|
|||||||
RUN CGO_ENABLED=1 GOOS=linux go build -o /app/backend/pocket-id-backend .
|
RUN CGO_ENABLED=1 GOOS=linux go build -o /app/backend/pocket-id-backend .
|
||||||
|
|
||||||
# Stage 3: Production Image
|
# Stage 3: Production Image
|
||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
# Delete default node user
|
# Delete default node user
|
||||||
RUN deluser --remove-home node
|
RUN deluser --remove-home node
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.
|
Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.
|
||||||
|
|
||||||
→ Try out the [Demo](https://pocket-id.eliasschneider.com)
|
→ Try out the [Demo](https://demo.pocket-id.org)
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/96ac549d-b897-404a-8811-f42b16ea58e2" width="1200"/>
|
<img src="https://github.com/user-attachments/assets/96ac549d-b897-404a-8811-f42b16ea58e2" width="1200"/>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht
|
|||||||
|
|
||||||
Pocket ID can be set up in multiple ways. The easiest and recommended way is to use Docker.
|
Pocket ID can be set up in multiple ways. The easiest and recommended way is to use Docker.
|
||||||
|
|
||||||
Visit the [documentation](https://stonith404.github.io/pocket-id) for the setup guide and more information.
|
Visit the [documentation](https://docs.pocket-id.org) for the setup guide and more information.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
PUBLIC_APP_URL=http://localhost
|
PUBLIC_APP_URL=http://localhost
|
||||||
|
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
|
||||||
DB_PROVIDER=sqlite
|
DB_PROVIDER=sqlite
|
||||||
|
# MAXMIND_LICENSE_KEY=fixme # needed for IP geolocation in the audit log
|
||||||
SQLITE_DB_PATH=data/pocket-id.db
|
SQLITE_DB_PATH=data/pocket-id.db
|
||||||
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
||||||
UPLOAD_PATH=data/uploads
|
UPLOAD_PATH=data/uploads
|
||||||
|
|||||||
@@ -3,56 +3,55 @@ module github.com/stonith404/pocket-id/backend
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v11 v11.2.2
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0
|
github.com/fxamacker/cbor/v2 v2.7.0
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1
|
github.com/go-co-op/gocron/v2 v2.15.0
|
||||||
github.com/go-playground/validator/v10 v10.22.1
|
github.com/go-ldap/ldap/v3 v3.4.10
|
||||||
|
github.com/go-playground/validator/v10 v10.24.0
|
||||||
github.com/go-webauthn/webauthn v0.11.2
|
github.com/go-webauthn/webauthn v0.11.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/time v0.6.0
|
golang.org/x/time v0.9.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.7
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|
||||||
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/bytedance/sonic v1.12.3 // indirect
|
github.com/bytedance/sonic v1.12.8 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 // 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.14 // indirect
|
github.com/go-webauthn/x v0.1.16 // indirect
|
||||||
github.com/goccy/go-json v0.10.3 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
github.com/google/go-tpm v0.9.1 // indirect
|
github.com/google/go-tpm v0.9.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/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 // indirect
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
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
|
||||||
@@ -62,12 +61,12 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.10.0 // indirect
|
golang.org/x/arch v0.13.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.36.4 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
134
backend/go.sum
134
backend/go.sum
@@ -4,24 +4,24 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
|||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
|
||||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0=
|
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
||||||
github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs=
|
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
||||||
@@ -34,16 +34,16 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
@@ -56,24 +56,24 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||||
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
|
github.com/go-webauthn/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8=
|
||||||
github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
|
github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
|
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||||
github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -85,20 +85,27 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
|
|||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/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/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
@@ -106,13 +113,13 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
|||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -124,8 +131,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/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=
|
||||||
@@ -145,8 +152,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/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=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 h1:UihPOz+oIJ5X0JsO7wEkL50fheCODsoZ9r86mJWfNMc=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2 h1:jG+FaCBv3h6GD5F+oenTfe3+0NmX8sCKjni5k3A5Dek=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1/go.mod h1:vPpFrres6g9B5+meBwAd9xnp335KFcLEFW7EqJxBHy0=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
@@ -162,14 +169,16 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
@@ -189,20 +198,19 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
|
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||||
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
|
||||||
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/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
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=
|
||||||
@@ -218,18 +226,15 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
|
||||||
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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
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=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
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/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=
|
||||||
@@ -243,10 +248,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
||||||
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
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=
|
||||||
@@ -264,12 +268,10 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
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/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
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=
|
||||||
@@ -277,8 +279,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/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=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 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=
|
||||||
@@ -288,8 +290,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Bootstrap() {
|
func Bootstrap() {
|
||||||
|
initApplicationImages()
|
||||||
|
|
||||||
db := newDatabase()
|
db := newDatabase()
|
||||||
appConfigService := service.NewAppConfigService(db)
|
appConfigService := service.NewAppConfigService(db)
|
||||||
|
|
||||||
initApplicationImages()
|
|
||||||
initRouter(db, appConfigService)
|
initRouter(db, appConfigService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) {
|
|||||||
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
||||||
jwtService := service.NewJwtService(appConfigService)
|
jwtService := service.NewJwtService(appConfigService)
|
||||||
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
||||||
userService := service.NewUserService(db, jwtService, auditLogService, emailService)
|
userService := service.NewUserService(db, jwtService, auditLogService, emailService, appConfigService)
|
||||||
customClaimService := service.NewCustomClaimService(db)
|
customClaimService := service.NewCustomClaimService(db)
|
||||||
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
||||||
testService := service.NewTestService(db, appConfigService)
|
testService := service.NewTestService(db, appConfigService)
|
||||||
userGroupService := service.NewUserGroupService(db)
|
userGroupService := service.NewUserGroupService(db, appConfigService)
|
||||||
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
||||||
|
|
||||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
||||||
|
|||||||
@@ -176,3 +176,11 @@ func (e *LdapUserGroupUpdateError) Error() string {
|
|||||||
return "LDAP user groups can't be updated"
|
return "LDAP user groups can't be updated"
|
||||||
}
|
}
|
||||||
func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type OidcAccessDeniedError struct{}
|
||||||
|
|
||||||
|
func (e *OidcAccessDeniedError) Error() string {
|
||||||
|
return "You're not allowed to access this service"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
oc := &OidcController{oidcService: oidcService, jwtService: jwtService}
|
oc := &OidcController{oidcService: oidcService, jwtService: jwtService}
|
||||||
|
|
||||||
group.POST("/oidc/authorize", jwtAuthMiddleware.Add(false), oc.authorizeHandler)
|
group.POST("/oidc/authorize", jwtAuthMiddleware.Add(false), oc.authorizeHandler)
|
||||||
group.POST("/oidc/authorize/new-client", jwtAuthMiddleware.Add(false), oc.authorizeNewClientHandler)
|
group.POST("/oidc/authorization-required", jwtAuthMiddleware.Add(false), oc.authorizationConfirmationRequiredHandler)
|
||||||
|
|
||||||
group.POST("/oidc/token", oc.createTokensHandler)
|
group.POST("/oidc/token", oc.createTokensHandler)
|
||||||
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
group.PUT("/oidc/clients/:id", jwtAuthMiddleware.Add(true), oc.updateClientHandler)
|
group.PUT("/oidc/clients/:id", jwtAuthMiddleware.Add(true), oc.updateClientHandler)
|
||||||
group.DELETE("/oidc/clients/:id", jwtAuthMiddleware.Add(true), oc.deleteClientHandler)
|
group.DELETE("/oidc/clients/:id", jwtAuthMiddleware.Add(true), oc.deleteClientHandler)
|
||||||
|
|
||||||
|
group.PUT("/oidc/clients/:id/allowed-user-groups", jwtAuthMiddleware.Add(true), oc.updateAllowedUserGroupsHandler)
|
||||||
group.POST("/oidc/clients/:id/secret", jwtAuthMiddleware.Add(true), oc.createClientSecretHandler)
|
group.POST("/oidc/clients/:id/secret", jwtAuthMiddleware.Add(true), oc.createClientSecretHandler)
|
||||||
|
|
||||||
group.GET("/oidc/clients/:id/logo", oc.getClientLogoHandler)
|
group.GET("/oidc/clients/:id/logo", oc.getClientLogoHandler)
|
||||||
@@ -57,25 +59,20 @@ func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oc *OidcController) authorizeNewClientHandler(c *gin.Context) {
|
func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Context) {
|
||||||
var input dto.AuthorizeOidcClientRequestDto
|
var input dto.AuthorizationRequiredDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
code, callbackURL, err := oc.oidcService.AuthorizeNewClient(input, c.GetString("userID"), c.ClientIP(), c.Request.UserAgent())
|
hasAuthorizedClient, err := oc.oidcService.HasAuthorizedClient(input.ClientID, c.GetString("userID"), input.Scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := dto.AuthorizeOidcClientResponseDto{
|
c.JSON(http.StatusOK, gin.H{"authorizationRequired": !hasAuthorizedClient})
|
||||||
Code: code,
|
|
||||||
CallbackURL: callbackURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||||
@@ -134,7 +131,7 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// Return a different DTO based on the user's role
|
// Return a different DTO based on the user's role
|
||||||
if c.GetBool("userIsAdmin") {
|
if c.GetBool("userIsAdmin") {
|
||||||
clientDto := dto.OidcClientDto{}
|
clientDto := dto.OidcClientWithAllowedUserGroupsDto{}
|
||||||
err = dto.MapStruct(client, &clientDto)
|
err = dto.MapStruct(client, &clientDto)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.JSON(http.StatusOK, clientDto)
|
c.JSON(http.StatusOK, clientDto)
|
||||||
@@ -191,7 +188,7 @@ func (oc *OidcController) createClientHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientDto dto.OidcClientDto
|
var clientDto dto.OidcClientWithAllowedUserGroupsDto
|
||||||
if err := dto.MapStruct(client, &clientDto); err != nil {
|
if err := dto.MapStruct(client, &clientDto); err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -223,7 +220,7 @@ func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientDto dto.OidcClientDto
|
var clientDto dto.OidcClientWithAllowedUserGroupsDto
|
||||||
if err := dto.MapStruct(client, &clientDto); err != nil {
|
if err := dto.MapStruct(client, &clientDto); err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -278,3 +275,25 @@ func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
|||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
|
||||||
|
var input dto.OidcUpdateAllowedUserGroupsDto
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcClient, err := oc.oidcService.UpdateAllowedUserGroups(c.Param("id"), input)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var oidcClientDto dto.OidcClientDto
|
||||||
|
if err := dto.MapStruct(oidcClient, &oidcClientDto); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, oidcClientDto)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils/cookie"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -184,7 +186,10 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
|
sessionDurationInMinutesParsed, _ := strconv.Atoi(uc.appConfigService.DbConfig.SessionDuration.Value)
|
||||||
|
maxAge := sessionDurationInMinutesParsed * 60
|
||||||
|
cookie.AddAccessTokenCookie(c, maxAge, token)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +206,10 @@ func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
|
sessionDurationInMinutesParsed, _ := strconv.Atoi(uc.appConfigService.DbConfig.SessionDuration.Value)
|
||||||
|
maxAge := sessionDurationInMinutesParsed * 60
|
||||||
|
cookie.AddAccessTokenCookie(c, maxAge, token)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import (
|
|||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils/cookie"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -42,12 +43,12 @@ func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
|
cookie.AddSessionIdCookie(c, int(options.Timeout.Seconds()), options.SessionID)
|
||||||
c.JSON(http.StatusOK, options.Response)
|
c.JSON(http.StatusOK, options.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) verifyRegistrationHandler(c *gin.Context) {
|
func (wc *WebauthnController) verifyRegistrationHandler(c *gin.Context) {
|
||||||
sessionID, err := c.Cookie("session_id")
|
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(&common.MissingSessionIdError{})
|
c.Error(&common.MissingSessionIdError{})
|
||||||
return
|
return
|
||||||
@@ -76,12 +77,12 @@ func (wc *WebauthnController) beginLoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
|
cookie.AddSessionIdCookie(c, int(options.Timeout.Seconds()), options.SessionID)
|
||||||
c.JSON(http.StatusOK, options.Response)
|
c.JSON(http.StatusOK, options.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
|
func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
|
||||||
sessionID, err := c.Cookie("session_id")
|
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(&common.MissingSessionIdError{})
|
c.Error(&common.MissingSessionIdError{})
|
||||||
return
|
return
|
||||||
@@ -105,7 +106,10 @@ func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.AddAccessTokenCookie(c, wc.appConfigService.DbConfig.SessionDuration.Value, token)
|
sessionDurationInMinutesParsed, _ := strconv.Atoi(wc.appConfigService.DbConfig.SessionDuration.Value)
|
||||||
|
maxAge := sessionDurationInMinutesParsed * 60
|
||||||
|
cookie.AddAccessTokenCookie(c, maxAge, token)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +169,6 @@ func (wc *WebauthnController) updateCredentialHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
||||||
utils.AddAccessTokenCookie(c, "0", "")
|
cookie.AddAccessTokenCookie(c, 0, "")
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ type OidcClientDto struct {
|
|||||||
CallbackURLs []string `json:"callbackURLs"`
|
CallbackURLs []string `json:"callbackURLs"`
|
||||||
IsPublic bool `json:"isPublic"`
|
IsPublic bool `json:"isPublic"`
|
||||||
PkceEnabled bool `json:"pkceEnabled"`
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
CreatedBy UserDto `json:"createdBy"`
|
}
|
||||||
|
|
||||||
|
type OidcClientWithAllowedUserGroupsDto struct {
|
||||||
|
PublicOidcClientDto
|
||||||
|
CallbackURLs []string `json:"callbackURLs"`
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
|
AllowedUserGroups []UserGroupDtoWithUserCount `json:"allowedUserGroups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientCreateDto struct {
|
type OidcClientCreateDto struct {
|
||||||
@@ -35,6 +42,11 @@ type AuthorizeOidcClientResponseDto struct {
|
|||||||
CallbackURL string `json:"callbackURL"`
|
CallbackURL string `json:"callbackURL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthorizationRequiredDto struct {
|
||||||
|
ClientID string `json:"clientID" binding:"required"`
|
||||||
|
Scope string `json:"scope" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type OidcCreateTokensDto struct {
|
type OidcCreateTokensDto struct {
|
||||||
GrantType string `form:"grant_type" binding:"required"`
|
GrantType string `form:"grant_type" binding:"required"`
|
||||||
Code string `form:"code" binding:"required"`
|
Code string `form:"code" binding:"required"`
|
||||||
@@ -42,3 +54,7 @@ type OidcCreateTokensDto struct {
|
|||||||
ClientSecret string `form:"client_secret"`
|
ClientSecret string `form:"client_secret"`
|
||||||
CodeVerifier string `form:"code_verifier"`
|
CodeVerifier string `form:"code_verifier"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OidcUpdateAllowedUserGroupsDto struct {
|
||||||
|
UserGroupIDs []string `json:"userGroupIds" binding:"required"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,3 @@ type UserGroupCreateDto struct {
|
|||||||
type UserGroupUpdateUsersDto struct {
|
type UserGroupUpdateUsersDto struct {
|
||||||
UserIDs []string `json:"userIds" binding:"required"`
|
UserIDs []string `json:"userIds" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssignUserToGroupDto struct {
|
|
||||||
UserID string `json:"userId" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils/cookie"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ func NewJwtAuthMiddleware(jwtService *service.JwtService, ignoreUnauthenticated
|
|||||||
func (m *JwtAuthMiddleware) Add(adminOnly bool) gin.HandlerFunc {
|
func (m *JwtAuthMiddleware) Add(adminOnly bool) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Extract the token from the cookie or the Authorization header
|
// Extract the token from the cookie or the Authorization header
|
||||||
token, err := c.Cookie("access_token")
|
token, err := c.Cookie(cookie.AccessTokenCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
authorizationHeaderSplitted := strings.Split(c.GetHeader("Authorization"), " ")
|
authorizationHeaderSplitted := strings.Split(c.GetHeader("Authorization"), " ")
|
||||||
if len(authorizationHeaderSplitted) == 2 {
|
if len(authorizationHeaderSplitted) == 2 {
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ type OidcClient struct {
|
|||||||
IsPublic bool
|
IsPublic bool
|
||||||
PkceEnabled bool
|
PkceEnabled bool
|
||||||
|
|
||||||
CreatedByID string
|
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||||
CreatedBy User
|
CreatedByID string
|
||||||
|
CreatedBy User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) {
|
func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
LdapEnabled: model.AppConfigVariable{
|
LdapEnabled: model.AppConfigVariable{
|
||||||
Key: "ldapEnabled",
|
Key: "ldapEnabled",
|
||||||
Type: "bool",
|
Type: "bool",
|
||||||
|
IsPublic: true,
|
||||||
DefaultValue: "false",
|
DefaultValue: "false",
|
||||||
},
|
},
|
||||||
LdapUrl: model.AppConfigVariable{
|
LdapUrl: model.AppConfigVariable{
|
||||||
|
|||||||
@@ -4,18 +4,20 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
htemplate "html/template"
|
htemplate "html/template"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
"mime/quotedprintable"
|
||||||
"net"
|
"net"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"os"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var netDialer = &net.Dialer{
|
var netDialer = &net.Dialer{
|
||||||
@@ -88,18 +90,33 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
)
|
)
|
||||||
c.Body(body)
|
c.Body(body)
|
||||||
|
|
||||||
// Set up the TLS configuration
|
// Connect to the SMTP server
|
||||||
|
client, err := srv.getSmtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
if err := srv.sendEmailContent(client, toEmail, c); err != nil {
|
||||||
|
return fmt.Errorf("send email content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *EmailService) getSmtpClient() (client *smtp.Client, err error) {
|
||||||
|
port := srv.appConfigService.DbConfig.SmtpPort.Value
|
||||||
|
smtpAddress := srv.appConfigService.DbConfig.SmtpHost.Value + ":" + port
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
InsecureSkipVerify: srv.appConfigService.DbConfig.SmtpSkipCertVerify.Value == "true",
|
InsecureSkipVerify: srv.appConfigService.DbConfig.SmtpSkipCertVerify.Value == "true",
|
||||||
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to the SMTP server
|
// Connect to the SMTP server
|
||||||
port := srv.appConfigService.DbConfig.SmtpPort.Value
|
|
||||||
smtpAddress := srv.appConfigService.DbConfig.SmtpHost.Value + ":" + port
|
|
||||||
var client *smtp.Client
|
|
||||||
if srv.appConfigService.DbConfig.SmtpTls.Value == "false" {
|
if srv.appConfigService.DbConfig.SmtpTls.Value == "false" {
|
||||||
client, err = smtp.Dial(smtpAddress)
|
client, err = srv.connectToSmtpServer(smtpAddress)
|
||||||
} else if port == "465" {
|
} else if port == "465" {
|
||||||
client, err = srv.connectToSmtpServerUsingImplicitTLS(
|
client, err = srv.connectToSmtpServerUsingImplicitTLS(
|
||||||
smtpAddress,
|
smtpAddress,
|
||||||
@@ -111,17 +128,14 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
tlsConfig,
|
tlsConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer client.Close()
|
// Set up the authentication if user or password are set
|
||||||
|
|
||||||
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
||||||
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
||||||
|
|
||||||
// Set up the authentication if user or password are set
|
|
||||||
if smtpUser != "" || smtpPassword != "" {
|
if smtpUser != "" || smtpPassword != "" {
|
||||||
auth := smtp.PlainAuth("",
|
auth := smtp.PlainAuth("",
|
||||||
srv.appConfigService.DbConfig.SmtpUser.Value,
|
srv.appConfigService.DbConfig.SmtpUser.Value,
|
||||||
@@ -129,16 +143,29 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
srv.appConfigService.DbConfig.SmtpHost.Value,
|
srv.appConfigService.DbConfig.SmtpHost.Value,
|
||||||
)
|
)
|
||||||
if err := client.Auth(auth); err != nil {
|
if err := client.Auth(auth); err != nil {
|
||||||
return fmt.Errorf("failed to authenticate SMTP client: %w", err)
|
return nil, fmt.Errorf("failed to authenticate SMTP client: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the email
|
return client, err
|
||||||
if err := srv.sendEmailContent(client, toEmail, c); err != nil {
|
}
|
||||||
return fmt.Errorf("send email content: %w", err)
|
|
||||||
|
func (srv *EmailService) connectToSmtpServer(serverAddr string) (*smtp.Client, error) {
|
||||||
|
conn, err := netDialer.Dial("tcp", serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if err := srv.sendHelloCommand(client); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
||||||
@@ -157,6 +184,10 @@ func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string,
|
|||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := srv.sendHelloCommand(client); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,12 +203,26 @@ func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tls
|
|||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := srv.sendHelloCommand(client); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := client.StartTLS(tlsConfig); err != nil {
|
if err := client.StartTLS(tlsConfig); err != nil {
|
||||||
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err == nil {
|
||||||
|
if err := client.Hello(hostname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *EmailService) sendEmailContent(client *smtp.Client, toEmail email.Address, c *email.Composer) error {
|
func (srv *EmailService) sendEmailContent(client *smtp.Client, toEmail email.Address, c *email.Composer) error {
|
||||||
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value); err != nil {
|
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value); err != nil {
|
||||||
return fmt.Errorf("failed to set sender: %w", err)
|
return fmt.Errorf("failed to set sender: %w", err)
|
||||||
|
|||||||
@@ -38,71 +38,111 @@ func NewOidcService(db *gorm.DB, jwtService *JwtService, appConfigService *AppCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) Authorize(input dto.AuthorizeOidcClientRequestDto, userID, ipAddress, userAgent string) (string, string, error) {
|
func (s *OidcService) Authorize(input dto.AuthorizeOidcClientRequestDto, userID, ipAddress, userAgent string) (string, string, error) {
|
||||||
var userAuthorizedOIDCClient model.UserAuthorizedOidcClient
|
|
||||||
s.db.Preload("Client").First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", input.ClientID, userID)
|
|
||||||
|
|
||||||
if userAuthorizedOIDCClient.Client.IsPublic && input.CodeChallenge == "" {
|
|
||||||
return "", "", &common.OidcMissingCodeChallengeError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if userAuthorizedOIDCClient.Scope != input.Scope {
|
|
||||||
return "", "", &common.OidcMissingAuthorizationError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackURL, err := s.getCallbackURL(userAuthorizedOIDCClient.Client, input.CallbackURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.auditLogService.Create(model.AuditLogEventClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": userAuthorizedOIDCClient.Client.Name})
|
|
||||||
|
|
||||||
return code, callbackURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OidcService) AuthorizeNewClient(input dto.AuthorizeOidcClientRequestDto, userID, ipAddress, userAgent string) (string, string, error) {
|
|
||||||
var client model.OidcClient
|
var client model.OidcClient
|
||||||
if err := s.db.First(&client, "id = ?", input.ClientID).Error; err != nil {
|
if err := s.db.Preload("AllowedUserGroups").First(&client, "id = ?", input.ClientID).Error; err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the client is not public, the code challenge must be provided
|
||||||
if client.IsPublic && input.CodeChallenge == "" {
|
if client.IsPublic && input.CodeChallenge == "" {
|
||||||
return "", "", &common.OidcMissingCodeChallengeError{}
|
return "", "", &common.OidcMissingCodeChallengeError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the callback URL of the client. Return an error if the provided callback URL is not allowed
|
||||||
callbackURL, err := s.getCallbackURL(client, input.CallbackURL)
|
callbackURL, err := s.getCallbackURL(client, input.CallbackURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
// Check if the user group is allowed to authorize the client
|
||||||
UserID: userID,
|
var user model.User
|
||||||
ClientID: input.ClientID,
|
if err := s.db.Preload("UserGroups").First(&user, "id = ?", userID).Error; err != nil {
|
||||||
Scope: input.Scope,
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&userAuthorizedClient).Error; err != nil {
|
if !s.IsUserGroupAllowedToAuthorize(user, client) {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
return "", "", &common.OidcAccessDeniedError{}
|
||||||
err = s.db.Model(&userAuthorizedClient).Update("scope", input.Scope).Error
|
}
|
||||||
} else {
|
|
||||||
return "", "", err
|
// Check if the user has already authorized the client with the given scope
|
||||||
|
hasAuthorizedClient, err := s.HasAuthorizedClient(input.ClientID, userID, input.Scope)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has not authorized the client, create a new authorization in the database
|
||||||
|
if !hasAuthorizedClient {
|
||||||
|
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||||
|
UserID: userID,
|
||||||
|
ClientID: input.ClientID,
|
||||||
|
Scope: input.Scope,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.Create(&userAuthorizedClient).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
// The client has already been authorized but with a different scope so we need to update the scope
|
||||||
|
if err := s.db.Model(&userAuthorizedClient).Update("scope", input.Scope).Error; err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the authorization code
|
||||||
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod)
|
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.auditLogService.Create(model.AuditLogEventNewClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name})
|
// Log the authorization event
|
||||||
|
if hasAuthorizedClient {
|
||||||
|
s.auditLogService.Create(model.AuditLogEventClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name})
|
||||||
|
} else {
|
||||||
|
s.auditLogService.Create(model.AuditLogEventNewClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return code, callbackURL, nil
|
return code, callbackURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasAuthorizedClient checks if the user has already authorized the client with the given scope
|
||||||
|
func (s *OidcService) HasAuthorizedClient(clientID, userID, scope string) (bool, error) {
|
||||||
|
var userAuthorizedOidcClient model.UserAuthorizedOidcClient
|
||||||
|
if err := s.db.First(&userAuthorizedOidcClient, "client_id = ? AND user_id = ?", clientID, userID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if userAuthorizedOidcClient.Scope != scope {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client
|
||||||
|
func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
|
||||||
|
if len(client.AllowedUserGroups) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowedToAuthorize := false
|
||||||
|
for _, userGroup := range client.AllowedUserGroups {
|
||||||
|
for _, userGroupUser := range user.UserGroups {
|
||||||
|
if userGroup.ID == userGroupUser.ID {
|
||||||
|
isAllowedToAuthorize = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAllowedToAuthorize
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret, codeVerifier string) (string, string, error) {
|
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret, codeVerifier string) (string, string, error) {
|
||||||
if grantType != "authorization_code" {
|
if grantType != "authorization_code" {
|
||||||
return "", "", &common.OidcGrantTypeNotSupportedError{}
|
return "", "", &common.OidcGrantTypeNotSupportedError{}
|
||||||
@@ -161,7 +201,7 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret, code
|
|||||||
|
|
||||||
func (s *OidcService) GetClient(clientID string) (model.OidcClient, error) {
|
func (s *OidcService) GetClient(clientID string) (model.OidcClient, error) {
|
||||||
var client model.OidcClient
|
var client model.OidcClient
|
||||||
if err := s.db.Preload("CreatedBy").First(&client, "id = ?", clientID).Error; err != nil {
|
if err := s.db.Preload("CreatedBy").Preload("AllowedUserGroups").First(&client, "id = ?", clientID).Error; err != nil {
|
||||||
return model.OidcClient{}, err
|
return model.OidcClient{}, err
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
@@ -382,6 +422,33 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
|||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OidcService) UpdateAllowedUserGroups(id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) {
|
||||||
|
client, err = s.GetClient(id)
|
||||||
|
if err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the user groups based on UserGroupIDs in input
|
||||||
|
var groups []model.UserGroup
|
||||||
|
if len(input.UserGroupIDs) > 0 {
|
||||||
|
if err := s.db.Where("id IN (?)", input.UserGroupIDs).Find(&groups).Error; err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the current user groups with the new set of user groups
|
||||||
|
if err := s.db.Model(&client).Association("AllowedUserGroups").Replace(groups); err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated client
|
||||||
|
if err := s.db.Save(&client).Error; err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OidcService) createAuthorizationCode(clientID string, userID string, scope string, nonce string, codeChallenge string, codeChallengeMethod string) (string, error) {
|
func (s *OidcService) createAuthorizationCode(clientID string, userID string, scope string, nonce string, codeChallenge string, codeChallengeMethod string) (string, error) {
|
||||||
randomString, err := utils.GenerateRandomAlphanumericString(32)
|
randomString, err := utils.GenerateRandomAlphanumericString(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -124,7 +124,10 @@ func (s *TestService) SeedDatabase() error {
|
|||||||
Name: "Immich",
|
Name: "Immich",
|
||||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||||
CallbackURLs: model.CallbackURLs{"http://immich/auth/callback"},
|
CallbackURLs: model.CallbackURLs{"http://immich/auth/callback"},
|
||||||
CreatedByID: users[0].ID,
|
CreatedByID: users[1].ID,
|
||||||
|
AllowedUserGroups: []model.UserGroup{
|
||||||
|
userGroups[1],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, client := range oidcClients {
|
for _, client := range oidcClients {
|
||||||
@@ -163,27 +166,31 @@ func (s *TestService) SeedDatabase() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
publicKey1, err := s.getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwcOo5KV169KR67QEHrcYkeXE3CCxv2BgwnSq4VYTQxyLtdmKxegexa8JdwFKhKXa2BMI9xaN15BoL6wSCRFJhg==")
|
// To generate a new key pair, run the following command:
|
||||||
publicKey2, err := s.getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESq/wR8QbBu3dKnpaw/v0mDxFFDwnJ/L5XHSg2tAmq5x1BpSMmIr3+DxCbybVvGRmWGh8kKhy7SMnK91M6rFHTA==")
|
// openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 | \
|
||||||
|
// openssl pkcs8 -topk8 -nocrypt | tee >(openssl pkey -pubout)
|
||||||
|
|
||||||
|
publicKeyPasskey1, err := s.getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwcOo5KV169KR67QEHrcYkeXE3CCxv2BgwnSq4VYTQxyLtdmKxegexa8JdwFKhKXa2BMI9xaN15BoL6wSCRFJhg==")
|
||||||
|
publicKeyPasskey2, err := s.getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEj4qA0PrZzg8Co1C27nyUbzrp8Ewjr7eOlGI2LfrzmbL5nPhZRAdJ3hEaqrHMSnJBhfMqtQGKwDYpaLIQFAKLhw==")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
webauthnCredentials := []model.WebauthnCredential{
|
webauthnCredentials := []model.WebauthnCredential{
|
||||||
{
|
{
|
||||||
Name: "Passkey 1",
|
Name: "Passkey 1",
|
||||||
CredentialID: []byte("test-credential-1"),
|
CredentialID: []byte("test-credential-tim"),
|
||||||
PublicKey: publicKey1,
|
PublicKey: publicKeyPasskey1,
|
||||||
AttestationType: "none",
|
AttestationType: "none",
|
||||||
Transport: model.AuthenticatorTransportList{protocol.Internal},
|
Transport: model.AuthenticatorTransportList{protocol.Internal},
|
||||||
UserID: users[0].ID,
|
UserID: users[0].ID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Passkey 2",
|
Name: "Passkey 2",
|
||||||
CredentialID: []byte("test-credential-2"),
|
CredentialID: []byte("test-credential-craig"),
|
||||||
PublicKey: publicKey2,
|
PublicKey: publicKeyPasskey2,
|
||||||
AttestationType: "none",
|
AttestationType: "none",
|
||||||
Transport: model.AuthenticatorTransportList{protocol.Internal},
|
Transport: model.AuthenticatorTransportList{protocol.Internal},
|
||||||
UserID: users[0].ID,
|
UserID: users[1].ID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, credential := range webauthnCredentials {
|
for _, credential := range webauthnCredentials {
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserGroupService struct {
|
type UserGroupService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
appConfigService *AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserGroupService(db *gorm.DB) *UserGroupService {
|
func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService) *UserGroupService {
|
||||||
return &UserGroupService{db: db}
|
return &UserGroupService{db: db, appConfigService: appConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGroupService) List(name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
func (s *UserGroupService) List(name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
||||||
@@ -51,7 +52,8 @@ func (s *UserGroupService) Delete(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if group.LdapID != nil {
|
// Disallow deleting the group if it is an LDAP group and LDAP is enabled
|
||||||
|
if group.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||||
return &common.LdapUserGroupUpdateError{}
|
return &common.LdapUserGroupUpdateError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,13 +85,13 @@ func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto, allow
|
|||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if group.LdapID != nil && !allowLdapUpdate {
|
// Disallow updating the group if it is an LDAP group and LDAP is enabled
|
||||||
|
if !allowLdapUpdate && group.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||||
return model.UserGroup{}, &common.LdapUserGroupUpdateError{}
|
return model.UserGroup{}, &common.LdapUserGroupUpdateError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Name = input.Name
|
group.Name = input.Name
|
||||||
group.FriendlyName = input.FriendlyName
|
group.FriendlyName = input.FriendlyName
|
||||||
group.LdapID = &input.LdapID
|
|
||||||
|
|
||||||
if err := s.db.Preload("Users").Save(&group).Error; err != nil {
|
if err := s.db.Preload("Users").Save(&group).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
jwtService *JwtService
|
jwtService *JwtService
|
||||||
auditLogService *AuditLogService
|
auditLogService *AuditLogService
|
||||||
emailService *EmailService
|
emailService *EmailService
|
||||||
|
appConfigService *AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService) *UserService {
|
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService) *UserService {
|
||||||
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService, emailService: emailService}
|
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService, emailService: emailService, appConfigService: appConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
||||||
@@ -52,7 +53,8 @@ func (s *UserService) DeleteUser(userID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.LdapID != nil {
|
// Disallow deleting the user if it is an LDAP user and LDAP is enabled
|
||||||
|
if user.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||||
return &common.LdapUserUpdateError{}
|
return &common.LdapUserUpdateError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +88,8 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
|||||||
return model.User{}, err
|
return model.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.LdapID != nil && !allowLdapUpdate {
|
// Disallow updating the user if it is an LDAP group and LDAP is enabled
|
||||||
|
if !allowLdapUpdate && user.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||||
return model.User{}, &common.LdapUserUpdateError{}
|
return model.User{}, &common.LdapUserUpdateError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
backend/internal/utils/cookie/add_cookie.go
Normal file
13
backend/internal/utils/cookie/add_cookie.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package cookie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddAccessTokenCookie(c *gin.Context, maxAgeInSeconds int, token string) {
|
||||||
|
c.SetCookie(AccessTokenCookieName, token, maxAgeInSeconds, "/", "", true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddSessionIdCookie(c *gin.Context, maxAgeInSeconds int, sessionID string) {
|
||||||
|
c.SetCookie(SessionIdCookieName, sessionID, maxAgeInSeconds, "/", "", true, true)
|
||||||
|
}
|
||||||
16
backend/internal/utils/cookie/cookie_names.go
Normal file
16
backend/internal/utils/cookie/cookie_names.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package cookie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AccessTokenCookieName = "__Host-access_token"
|
||||||
|
var SessionIdCookieName = "__Host-session"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if strings.HasPrefix(common.EnvConfig.AppURL, "http://") {
|
||||||
|
AccessTokenCookieName = "access_token"
|
||||||
|
SessionIdCookieName = "session"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddAccessTokenCookie(c *gin.Context, sessionDurationInMinutes string, token string) {
|
|
||||||
sessionDurationInMinutesParsed, _ := strconv.Atoi(sessionDurationInMinutes)
|
|
||||||
maxAge := sessionDurationInMinutesParsed * 60
|
|
||||||
c.SetCookie("access_token", token, maxAge, "/", "", true, true)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE oidc_clients_allowed_user_groups;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE oidc_clients_allowed_user_groups
|
||||||
|
(
|
||||||
|
user_group_id UUID NOT NULL REFERENCES user_groups ON DELETE CASCADE,
|
||||||
|
oidc_client_id UUID NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (oidc_client_id, user_group_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE oidc_clients_allowed_user_groups;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE oidc_clients_allowed_user_groups
|
||||||
|
(
|
||||||
|
user_group_id TEXT NOT NULL,
|
||||||
|
oidc_client_id TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (oidc_client_id, user_group_id),
|
||||||
|
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';
|
||||||
63
docs/docs/client-examples/freshrss.md
Normal file
63
docs/docs/client-examples/freshrss.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
id: freshrss
|
||||||
|
---
|
||||||
|
|
||||||
|
# FreshRSS
|
||||||
|
|
||||||
|
The following example variables are used, and should be replaced with your actual URLs.
|
||||||
|
|
||||||
|
- `freshrss.example.com` (The URL of your Proxmox instance.)
|
||||||
|
- `id.example.com` (The URL of your Pocket ID instance.)
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket ID create a new OIDC Client, name it, for example, `FreshRSS`.
|
||||||
|
2. Set a logo for this OIDC Client if you would like to.
|
||||||
|
3. Set the callback URL to: `https://freshrss.example.com`.
|
||||||
|
4. Copy the `Client ID`, `Client Secret`, and `OIDC Discovery URL` for use in the next steps.
|
||||||
|
|
||||||
|
## FreshRSS Setup
|
||||||
|
|
||||||
|
See [FreshRSS’ OpenID Connect documentation](16_OpenID-Connect.md) for general OIDC settings.
|
||||||
|
|
||||||
|
This is an example docker-compose file for FreshRSS with OIDC enabled.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
freshrss:
|
||||||
|
image: freshrss/freshrss:1.25.0
|
||||||
|
container_name: freshrss
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
volumes:
|
||||||
|
- /freshrss_data:/var/www/FreshRSS/data
|
||||||
|
- /freshrss_extensions:/var/www/FreshRSS/extensions
|
||||||
|
environment:
|
||||||
|
CRON_MIN: 1,31
|
||||||
|
TZ: Etc/UTC
|
||||||
|
OIDC_ENABLED: 1
|
||||||
|
OIDC_CLIENT_ID: <POCKET_ID_CLIENT_ID>
|
||||||
|
OIDC_CLIENT_SECRET: <POCKET_ID_SECRET>
|
||||||
|
OIDC_PROVIDER_METADATA_URL: https://id.example.com/.well-known/openid-configuration
|
||||||
|
OIDC_SCOPES: openid email profile
|
||||||
|
OIDC_X_FORWARDED_HEADERS: X-Forwarded-Proto X-Forwarded-Host
|
||||||
|
OIDC_REMOTE_USER_CLAIM: preferred_username
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- freshrss
|
||||||
|
networks:
|
||||||
|
freshrss:
|
||||||
|
name: freshrss
|
||||||
|
```
|
||||||
|
|
||||||
|
:::important
|
||||||
|
The Username used in Pocket ID must match the Username used in FreshRSS **exactly**. This also applies to case sensitivity. As of version `0.24` of Pocket ID all Usernames are required to be entirely lowercase. FreshRSS allows for uppercase. If a Pocket ID Username is `amanda` and your FreshRSS Username is `Amanda`, you will get a 403 error in FreshRSS and be unable to login. As of version `1.25` of FreshRSS, you are unable to change your username in the GUI. To change your FreshRSS username to lowercase or to match your Pocket ID username, you must nagivate to your FreshRSS volume location. Go to `data/users/` and change the folder for your user to the matching username in Pocket ID, then restart the FreshRSS container to apply the changes.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Complete OIDC Setup
|
||||||
|
|
||||||
|
If you are setting up a new instance of FreshRSS, simply start the container with the OIDC variables and navigate to your FreshRSS URL.
|
||||||
|
|
||||||
|
If you are adding OIDC to an existing FreshRSS instance, recreate the container with the docker-compose file with the OIDC variables in it and navigate to your FreshRSS URL. Go to `Settings > Authentication` and change the Authentication method to **HTTP** and hit Submit. Logout to test your OIDC connection.
|
||||||
|
|
||||||
|
If you have an error with Pocket ID or are unable to login to your FreshRSS account, you can revert to password login by editing your `config.php` file for FreshRSS. Find the value for `auth_type` and change from `http_auth` to `form`. Restart the FreshRSS container to revert to password login.
|
||||||
30
docs/docs/client-examples/gitea.md
Normal file
30
docs/docs/client-examples/gitea.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
id: gitea
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitea
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket ID, create a new OIDC client named `Gitea` (or any name you prefer).
|
||||||
|
2. (Optional) Set a logo for the OIDC client.
|
||||||
|
3. Set the callback URL to: `https://<Gitea Host>/user/oauth2/PocketID/callback`
|
||||||
|
4. Copy the `Client ID`, `Client Secret`, and `OIDC Discovery URL` for the next steps.
|
||||||
|
|
||||||
|
## Gitea Setup
|
||||||
|
|
||||||
|
1. Log in to Gitea as an admin.
|
||||||
|
2. Go to **Site Administration → Identity & Access → Authentication Sources**.
|
||||||
|
3. Click **Add Authentication Source**.
|
||||||
|
4. Set **Authentication Type** to `OAuth2`.
|
||||||
|
5. Set **Authentication Name** to `PocketID`.
|
||||||
|
:::important
|
||||||
|
If you change this name, update the callback URL in Pocket ID to match.
|
||||||
|
:::
|
||||||
|
6. Set **OAuth2 Provider** to `OpenID Connect`.
|
||||||
|
7. Enter the `Client ID` into the **Client ID (Key)** field.
|
||||||
|
8. Enter the `Client Secret` into the **Client Secret** field.
|
||||||
|
9. Enter the `OIDC Discovery URL` into the **OpenID Connect Auto Discovery URL** field.
|
||||||
|
10. Enable **Skip local 2FA**.
|
||||||
|
11. Set **Additional Scopes** to `openid email profile`.
|
||||||
|
12. Save the settings and test the OAuth login.
|
||||||
22
docs/docs/client-examples/grist.md
Normal file
22
docs/docs/client-examples/grist.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
id: grist
|
||||||
|
---
|
||||||
|
|
||||||
|
# Grist
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
1. In Pocket-ID create a new OIDC Client, name it i.e. `Grist`
|
||||||
|
2. Set the callback url to: `https://<Grist Host>/oauth2/callback`
|
||||||
|
3. In Grist (Docker/Docker Compose/etc), set these environment variables:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
GRIST_OIDC_IDP_ISSUER="https://<Pocket ID Host>/.well-known/openid-configuration"
|
||||||
|
GRIST_OIDC_IDP_CLIENT_ID="<Client ID from the OIDC Client created in Pocket ID>"
|
||||||
|
GRIST_OIDC_IDP_CLIENT_SECRET="<Client Secret from the OIDC Client created in Pocket ID>"
|
||||||
|
GRIST_OIDC_SP_HOST="https://<Grist Host>"
|
||||||
|
GRIST_OIDC_IDP_SCOPES="openid email profile" # Default
|
||||||
|
GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true # Default=false, needs to be true for Pocket Id b/c end_session_endpoint is not implemented
|
||||||
|
GRIST_OIDC_IDP_END_SESSION_ENDPOINT="https://<Pocket ID Host>/api/webauthn/logout" # Only set this if GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=false and you need to define a custom endpoint
|
||||||
|
```
|
||||||
|
4. Also ensure that the `GRIST_DEFAULT_EMAIL` env variable is set to the same email address as your user profile within Pocket ID
|
||||||
|
5. Start/Restart Grist
|
||||||
34
docs/docs/client-examples/headscale.md
Normal file
34
docs/docs/client-examples/headscale.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
id: headscale
|
||||||
|
---
|
||||||
|
# Headscale
|
||||||
|
|
||||||
|
## Create OIDC Client in Pocket ID
|
||||||
|
1. Create a new OIDC Client in Pocket ID (e.g., `Headscale`).
|
||||||
|
2. Set the callback URL: `https://<HEADSCALE-DOMAIN>/oidc/callback`
|
||||||
|
3. Enable `PKCE`.
|
||||||
|
4. Copy the **Client ID** and **Client Secret**.
|
||||||
|
|
||||||
|
## Configure Headscale
|
||||||
|
> Refer to the example [`config.yaml`](https://github.com/juanfont/headscale/blob/main/config-example.yaml) for full OIDC configuration options.
|
||||||
|
|
||||||
|
Add the following to `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
oidc:
|
||||||
|
issuer: "https://<POCKET-ID-DOMAIN>"
|
||||||
|
client_id: "<CLIENT-ID>"
|
||||||
|
client_secret: "<CLIENT-SECRET>"
|
||||||
|
pkce:
|
||||||
|
enabled: true
|
||||||
|
method: S256
|
||||||
|
```
|
||||||
|
|
||||||
|
### (Optional) Restrict Access to Certain Groups
|
||||||
|
To allow only specific groups, add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scope: ["openid", "profile", "email", "groups"]
|
||||||
|
allowed_groups:
|
||||||
|
- <POCKET-ID-GROUP-NAME> #example: headscale
|
||||||
|
```
|
||||||
26
docs/docs/client-examples/immich.md
Normal file
26
docs/docs/client-examples/immich.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
id: immich
|
||||||
|
---
|
||||||
|
# Immich
|
||||||
|
|
||||||
|
## Create OIDC Client in Pocket ID
|
||||||
|
1. Create a new OIDC Client in Pocket ID (e.g., `immich`).
|
||||||
|
2. Set the callback URLs:
|
||||||
|
```
|
||||||
|
https://<IMMICH-DOMAIN>/auth/login
|
||||||
|
https://<IMMICH-DOMAIN>/user-settings
|
||||||
|
app.immich:///oauth-callback
|
||||||
|
```
|
||||||
|
4. Copy the **Client ID**, **Client Secret**, and **OIDC Discovery URL**.
|
||||||
|
|
||||||
|
## Configure Immich
|
||||||
|
1. Open Immich and navigate to:
|
||||||
|
**`Administration > Settings > Authentication Settings > OAuth`**
|
||||||
|
2. Enable **Login with OAuth**.
|
||||||
|
3. Fill in the required fields:
|
||||||
|
- **Issuer URL**: Paste the `Authorization URL` from Pocket ID.
|
||||||
|
- **Client ID**: Paste the `Client ID` from Pocket ID.
|
||||||
|
- **Client Secret**: Paste the `Client Secret` from Pocket ID.
|
||||||
|
4. *(Optional)* Change `Button Text` to `Login with Pocket ID`.
|
||||||
|
5. Save the settings.
|
||||||
|
6. Test the OAuth login to ensure it works.
|
||||||
28
docs/docs/client-examples/memos.md
Normal file
28
docs/docs/client-examples/memos.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
id: memos
|
||||||
|
---
|
||||||
|
|
||||||
|
# Memos
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket ID, create a new OIDC client named `Memos` (or any name you prefer).
|
||||||
|
2. (Optional) Set a logo for the OIDC client.
|
||||||
|
3. Set the callback URL to: `https://< Memos Host >/auth/callback`
|
||||||
|
4. Copy the `Client ID`, `Client Secret`, `Authorization endpoint`, `Token endpoint`, and `User endpoint` for the next steps.
|
||||||
|
|
||||||
|
## Gitea Setup
|
||||||
|
|
||||||
|
1. Log in to Memos as an admin.
|
||||||
|
2. Go to **Settings → SSO → Create**.
|
||||||
|
3. Set **Template** to `Custom`.
|
||||||
|
4. Enter the `Client ID` into the **Client ID** field.
|
||||||
|
5. Enter the `Client Secret` into the **Client secret** field.
|
||||||
|
6. Enter the `Authorization URL` into the **Authorization endpoint** field.
|
||||||
|
7. Enter the `Token URL` into the **Token endpoint** field.
|
||||||
|
8. Enter the `Userinfo URL` into the **User endpoint** field.
|
||||||
|
11. Set **Scopes** to `openid email profile`.
|
||||||
|
12. Set **Identifier** to `preferred_username`
|
||||||
|
13. Set **Display Name** to `profile`.
|
||||||
|
14. Set **Email** to `email`.
|
||||||
|
15. Save the settings and test the OAuth login.
|
||||||
46
docs/docs/client-examples/netbox.md
Normal file
46
docs/docs/client-examples/netbox.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
id: netbox
|
||||||
|
---
|
||||||
|
|
||||||
|
# Netbox
|
||||||
|
|
||||||
|
**This guide does not currently show how to map groups in netbox from OIDC claims**
|
||||||
|
|
||||||
|
The following example variables are used, and should be replaced with your actual URLS.
|
||||||
|
|
||||||
|
- netbox.example.com (The url of your netbox instance.)
|
||||||
|
- id.example.com (The url of your Pocket ID instance.)
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket-ID create a new OIDC Client, name it i.e. `Netbox`.
|
||||||
|
2. Set a logo for this OIDC Client if you would like too.
|
||||||
|
3. Set the callback URL to: `https://netbox.example.com/oauth/complete/oidc/`.
|
||||||
|
4. Copy the `Client ID`, and the `Client Secret` for use in the next steps.
|
||||||
|
|
||||||
|
## Netbox Setup
|
||||||
|
|
||||||
|
This guide assumes you are using the git based install of netbox.
|
||||||
|
|
||||||
|
1. On your netbox server navigate to `/opt/netbox/netbox/netbox`
|
||||||
|
2. Add the following to your `configuration.py` file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Remote authentication support
|
||||||
|
REMOTE_AUTH_ENABLED = True
|
||||||
|
REMOTE_AUTH_BACKEND = 'social_core.backends.open_id_connect.OpenIdConnectAuth'
|
||||||
|
REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER'
|
||||||
|
REMOTE_AUTH_USER_FIRST_NAME = 'HTTP_REMOTE_USER_FIRST_NAME'
|
||||||
|
REMOTE_AUTH_USER_LAST_NAME = 'HTTP_REMOTE_USER_LAST_NAME'
|
||||||
|
REMOTE_AUTH_USER_EMAIL = 'HTTP_REMOTE_USER_EMAIL'
|
||||||
|
REMOTE_AUTH_AUTO_CREATE_USER = True
|
||||||
|
REMOTE_AUTH_DEFAULT_GROUPS = []
|
||||||
|
REMOTE_AUTH_DEFAULT_PERMISSIONS = {}
|
||||||
|
|
||||||
|
SOCIAL_AUTH_OIDC_ENDPOINT = 'https://id.example.com'
|
||||||
|
SOCIAL_AUTH_OIDC_KEY = '<client id from the first part of this guide>'
|
||||||
|
SOCIAL_AUTH_OIDC_SECRET = '<client id from the first part of this guide>'
|
||||||
|
LOGOUT_REDIRECT_URL = 'https://netbox.example.com'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Save the file and restart netbox: `sudo systemctl start netbox netbox-rq`
|
||||||
42
docs/docs/client-examples/pgadmin.md
Normal file
42
docs/docs/client-examples/pgadmin.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
id: pgadmin
|
||||||
|
---
|
||||||
|
|
||||||
|
# pgAdmin
|
||||||
|
|
||||||
|
The following example variables are used, and should be replaced with your actual URLS.
|
||||||
|
|
||||||
|
- pgadmin.example.com (The url of your pgAdmin instance.)
|
||||||
|
- id.example.com (The url of your Pocket ID instance.)
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket-ID create a new OIDC Client, name it i.e. `pgAdmin`.
|
||||||
|
2. Set a logo for this OIDC Client if you would like too.
|
||||||
|
3. Set the callback URL to: `https://pgadmin.example.com/oauth2/authorize`.
|
||||||
|
4. Copy the `Client ID`, `Client Secret`, `Authorization URL`, `Userinfo URL`, `Token URL`, and `OIDC Discovery URL` for use in the next steps.
|
||||||
|
|
||||||
|
# pgAdmin Setup
|
||||||
|
|
||||||
|
1. Add the following to the `config_local.py` file for pgAdmin:
|
||||||
|
|
||||||
|
**Make sure to replace https://id.example.com with your actual Pocket ID URL**
|
||||||
|
|
||||||
|
```python
|
||||||
|
AUTHENTICATION_SOURCES = ['oauth2', 'internal'] # This keeps internal authentication enabled as well as oauth2
|
||||||
|
OAUTH2_AUTO_CREATE_USER = True
|
||||||
|
OAUTH2_CONFIG = [{
|
||||||
|
'OAUTH2_NAME' : 'pocketid',
|
||||||
|
'OAUTH2_DISPLAY_NAME' : 'Pocket ID',
|
||||||
|
'OAUTH2_CLIENT_ID' : '<client id from the earlier step>',
|
||||||
|
'OAUTH2_CLIENT_SECRET' : '<client secret from the earlier step>',
|
||||||
|
'OAUTH2_TOKEN_URL' : 'https://id.example.com/api/oidc/token',
|
||||||
|
'OAUTH2_AUTHORIZATION_URL' : 'https://id.example/authorize',
|
||||||
|
'OAUTH2_API_BASE_URL' : 'https://id.example.com',
|
||||||
|
'OAUTH2_USERINFO_ENDPOINT' : 'https://id.example.com/api/oidc/userinfo',
|
||||||
|
'OAUTH2_SERVER_METADATA_URL' : 'https://id.example.com/.well-known/openid-configuration',
|
||||||
|
'OAUTH2_SCOPE' : 'openid email profile',
|
||||||
|
'OAUTH2_ICON' : 'fa-openid',
|
||||||
|
'OAUTH2_BUTTON_COLOR' : '#fd4b2d' # Can select any color you would like here.
|
||||||
|
}]
|
||||||
|
```
|
||||||
38
docs/docs/client-examples/portainer.md
Normal file
38
docs/docs/client-examples/portainer.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
id: portainer
|
||||||
|
---
|
||||||
|
|
||||||
|
# Portainer
|
||||||
|
|
||||||
|
**This requires Portainers Business Edition**
|
||||||
|
|
||||||
|
The following example variables are used, and should be replaced with your actual URLS.
|
||||||
|
|
||||||
|
- portainer.example.com (The url of your Portainer instance.)
|
||||||
|
- id.example.com (The url of your Pocket ID instance.)
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket-ID create a new OIDC Client, name it i.e. `Portainer`.
|
||||||
|
2. Set a logo for this OIDC Client if you would like too.
|
||||||
|
3. Set the callback URL to: `https://portainer.example.com/`.
|
||||||
|
4. Copy the `Client ID`, `Client Secret`, `Authorization URL`, `Userinfo URL`, and `Token URL` for use in the next steps.
|
||||||
|
|
||||||
|
# Portainer Setup
|
||||||
|
|
||||||
|
- While initally setting up OAuth in Portainer, its recommended to keep the `Hide internal authentication prompt` set to `Off` incase you need a fallback login
|
||||||
|
- This guide does **NOT** cover how to setup group claims in Portainer.
|
||||||
|
|
||||||
|
1. Open the Portainer web interface and navigate to: `Settings > Authentication`
|
||||||
|
2. Select `Custom OAuth Provider`
|
||||||
|
3. Paste the `Client ID` from Pocket ID into the `Client ID` field in Portainer.
|
||||||
|
4. Paste the `Client Secret` from Pocket ID into the `Client Secret` field in Portainer.
|
||||||
|
5. Paste the `Authorization URL` from Pocket ID into the `Authorization URL` field in Portainer.
|
||||||
|
6. Paste the `Token URL` from Pocket ID into the `Access token URL` field in Portainer.
|
||||||
|
7. Paste the `Userinfo URL` from Pocket ID into the `Resource URL` field in Portainer.
|
||||||
|
8. Set the `Redirect URL` to `https://portainer.example.com`
|
||||||
|
9. Set the `Logout URL` to `https://portainer.example.com`
|
||||||
|
10. Set the `User identifier` field to `preferred_username`. (This will use the users username vs the email)
|
||||||
|
11. Set the `Scopes` field to: `email openid profile`
|
||||||
|
12. Set `Auth Style` to `Auto detect`
|
||||||
|
13. Save the settings and test the new OAuth Login.
|
||||||
30
docs/docs/client-examples/proxmox.md
Normal file
30
docs/docs/client-examples/proxmox.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
id: proxmox
|
||||||
|
---
|
||||||
|
|
||||||
|
# Proxmox
|
||||||
|
|
||||||
|
The following example variables are used, and should be replaced with your actual URLs.
|
||||||
|
|
||||||
|
- `proxmox.example.com` (The URL of your Proxmox instance.)
|
||||||
|
- `id.example.com` (The URL of your Pocket ID instance.)
|
||||||
|
|
||||||
|
## Pocket ID Setup
|
||||||
|
|
||||||
|
1. In Pocket ID create a new OIDC Client, name it, for example, `Proxmox`.
|
||||||
|
2. Set a logo for this OIDC Client if you would like to.
|
||||||
|
3. Set the callback URL to: `https://proxmox.example.com`.
|
||||||
|
4. Copy the `Client ID`, and the `Client Secret` for use in the next steps.
|
||||||
|
|
||||||
|
## Proxmox Setup
|
||||||
|
|
||||||
|
1. Open the Proxmox console and navigate to: `Datacenter` -> `Permissions` -> `Realms`.
|
||||||
|
2. Add a new `OpenID Connect Server` Realm.
|
||||||
|
3. Enter `https://id.example.com` for the `Issuer URL`.
|
||||||
|
4. Enter a name for the realm of your choice, for example, `PocketID`.
|
||||||
|
5. Paste the `Client ID` from Pocket ID into the `Client ID` field in Proxmox.
|
||||||
|
6. Paste the `Client Secret` from Pocket ID into the `Client Key` field in Proxmox.
|
||||||
|
7. You can check the `Default` box if you want this to be the default realm Proxmox uses when signing in.
|
||||||
|
8. Check the `Autocreate Users` checkbox. (This will automatically create users in Proxmox if they don't exist).
|
||||||
|
9. Select `username` for the `Username Claim` dropdown. (This is a personal preference and controls how the username is shown, for example: `username = username@PocketID` or `email = username@example@PocketID`).
|
||||||
|
10. Leave the rest as defaults and click `OK` to save the new realm.
|
||||||
@@ -13,7 +13,7 @@ Pocket ID can sync users and groups from an LDAP Source (lldap, OpenLDAP, Active
|
|||||||
|
|
||||||
### Generic LDAP Setup
|
### Generic LDAP Setup
|
||||||
|
|
||||||
1. Follow the installation guide [here](/pocket-id/setup/installation).
|
1. Follow the installation guide [here](/setup/installation).
|
||||||
2. Once you have signed in with the initial admin account, navigate to the Application Configuration section at `https://pocket.id/settings/admin/application-configuration`.
|
2. Once you have signed in with the initial admin account, navigate to the Application Configuration section at `https://pocket.id/settings/admin/application-configuration`.
|
||||||
3. Client Configuration Setup
|
3. Client Configuration Setup
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ With [caddy-security](https://github.com/greenpau/caddy-security) you can easily
|
|||||||
|
|
||||||
#### 1. Create a new OIDC client in Pocket ID.
|
#### 1. Create a new OIDC client in Pocket ID.
|
||||||
|
|
||||||
Create a new OIDC client in Pocket ID by navigating to `https://<your-domain>/settings/admin/oidc-clients`. Now enter `https://<domain-of-proxied-service>/auth/oauth2/generic/authorization-code-callback` as the callback URL. After adding the client, you will obtain the client ID and client secret, which you will need in the next step.
|
Create a new OIDC client in Pocket ID by navigating to `https://<your-domain>/settings/admin/oidc-clients`. Now enter `https://<domain-of-proxied-service>/caddy-security/oauth2/generic/authorization-code-callback` as the callback URL. After adding the client, you will obtain the client ID and client secret, which you will need in the next step.
|
||||||
|
|
||||||
#### 2. Install caddy-security
|
#### 2. Install caddy-security
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ caddy add-package github.com/greenpau/caddy-security
|
|||||||
}
|
}
|
||||||
|
|
||||||
authorization policy mypolicy {
|
authorization policy mypolicy {
|
||||||
set auth url /auth/oauth2/generic
|
set auth url /caddy-security/oauth2/generic
|
||||||
allow roles user
|
allow roles user
|
||||||
inject headers with claims
|
inject headers with claims
|
||||||
}
|
}
|
||||||
@@ -75,8 +75,7 @@ caddy add-package github.com/greenpau/caddy-security
|
|||||||
|
|
||||||
https://<domain-of-your-service> {
|
https://<domain-of-your-service> {
|
||||||
@auth {
|
@auth {
|
||||||
path /auth/oauth2/generic
|
path /caddy-security/*
|
||||||
path /auth/oauth2/generic/authorization-code-callback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
route @auth {
|
route @auth {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
---
|
|
||||||
id: contribute
|
|
||||||
---
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
I am happy that you want to contribute to Pocket ID and help to make it better! All contributions are welcome, including issues, suggestions, pull requests and more.
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
You've found a bug, have suggestion or something else, just create an issue on GitHub and we can get in touch.
|
|
||||||
|
|
||||||
## Submit a Pull Request
|
|
||||||
|
|
||||||
Before you submit the pull request for review please ensure that
|
|
||||||
|
|
||||||
- The pull request naming follows the [Conventional Commits specification](https://www.conventionalcommits.org):
|
|
||||||
|
|
||||||
`<type>[optional scope]: <description>`
|
|
||||||
|
|
||||||
example:
|
|
||||||
|
|
||||||
```
|
|
||||||
feat(share): add password protection
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `TYPE` can be:
|
|
||||||
|
|
||||||
- **feat** - is a new feature
|
|
||||||
- **doc** - documentation only changes
|
|
||||||
- **fix** - a bug fix
|
|
||||||
- **refactor** - code change that neither fixes a bug nor adds a feature
|
|
||||||
|
|
||||||
- Your pull request has a detailed description
|
|
||||||
- You run `npm run format` to format the code
|
|
||||||
|
|
||||||
## Setup project
|
|
||||||
|
|
||||||
Pocket ID consists of a frontend, backend and a reverse proxy.
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
The backend is built with [Gin](https://gin-gonic.com) and written in Go.
|
|
||||||
|
|
||||||
#### Setup
|
|
||||||
|
|
||||||
1. Open the `backend` folder
|
|
||||||
2. Copy the `.env.example` file to `.env` and change the `APP_ENV` to `development`
|
|
||||||
3. Start the backend with `go run cmd/main.go`
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in TypeScript.
|
|
||||||
|
|
||||||
#### Setup
|
|
||||||
|
|
||||||
1. Open the `frontend` folder
|
|
||||||
2. Copy the `.env.example` file to `.env`
|
|
||||||
3. Install the dependencies with `npm install`
|
|
||||||
4. Start the frontend with `npm run dev`
|
|
||||||
|
|
||||||
You're all set!
|
|
||||||
|
|
||||||
### Reverse Proxy
|
|
||||||
We use [Caddy](https://caddyserver.com) as a reverse proxy. You can use any other reverse proxy if you want but you have to configure it yourself.
|
|
||||||
|
|
||||||
#### Setup
|
|
||||||
Run `caddy run --config reverse-proxy/Caddyfile` in the root folder.
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
We are using [Playwright](https://playwright.dev) for end-to-end testing.
|
|
||||||
|
|
||||||
The tests can be run like this:
|
|
||||||
1. Start the backend normally
|
|
||||||
2. Start the frontend in production mode with `npm run build && node build/index.js`
|
|
||||||
3. Run the tests with `npm run test`
|
|
||||||
@@ -14,11 +14,11 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht
|
|||||||
|
|
||||||
## Get to know Pocket ID
|
## Get to know Pocket ID
|
||||||
|
|
||||||
→ [Try the Demo of Pocket ID](https://pocket-id.eliasschneider.com/)<br/>
|
→ [Try the Demo of Pocket ID](https://demo.pocket-id.org)
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/96ac549d-b897-404a-8811-f42b16ea58e2" width="700"/>
|
<img src="https://github.com/user-attachments/assets/96ac549d-b897-404a-8811-f42b16ea58e2" width="700"/>
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
- [Installation](/pocket-id/setup/installation)
|
- [Installation](/setup/installation)
|
||||||
- [Proxy Services](/pocket-id/guides/proxy-services)
|
- [Proxy Services](/guides/proxy-services)
|
||||||
- [Client Examples](/pocket-id/client-examples)
|
- [Client Examples](/client-examples)
|
||||||
@@ -18,11 +18,23 @@ Pocket ID requires a [secure context](https://developer.mozilla.org/en-US/docs/W
|
|||||||
curl -o .env https://raw.githubusercontent.com/stonith404/pocket-id/main/.env.example
|
curl -o .env https://raw.githubusercontent.com/stonith404/pocket-id/main/.env.example
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Edit the `.env` file so that it fits your needs. See the [environment variables](/pocket-id/configuration/environment-variables) section for more information.
|
2. Edit the `.env` file so that it fits your needs. See the [environment variables](/configuration/environment-variables) section for more information.
|
||||||
3. Run `docker compose up -d`
|
3. Run `docker compose up -d`
|
||||||
|
|
||||||
You can now sign in with the admin account on `http://localhost/login/setup`.
|
You can now sign in with the admin account on `http://localhost/login/setup`.
|
||||||
|
|
||||||
|
### Proxmox
|
||||||
|
|
||||||
|
Run the [helper script](https://community-scripts.github.io/ProxmoxVE/scripts?id=pocketid) as root in your Proxmox shell.
|
||||||
|
|
||||||
|
**Configuration Paths**
|
||||||
|
- /opt/pocket-id/backend/.env
|
||||||
|
- /opt/pocket-id/frontend/.env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/pocketid.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
### Unraid
|
### Unraid
|
||||||
|
|
||||||
Pocket ID is available as a template on the Community Apps store.
|
Pocket ID is available as a template on the Community Apps store.
|
||||||
@@ -31,7 +43,7 @@ Pocket ID is available as a template on the Community Apps store.
|
|||||||
|
|
||||||
Required tools:
|
Required tools:
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/download/) >= 20
|
- [Node.js](https://nodejs.org/en/download/) >= 22
|
||||||
- [Go](https://golang.org/doc/install) >= 1.23
|
- [Go](https://golang.org/doc/install) >= 1.23
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [PM2](https://pm2.keymetrics.io/)
|
- [PM2](https://pm2.keymetrics.io/)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ docker compose up -d
|
|||||||
cd ../frontend
|
cd ../frontend
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
pm2 start build/index.js --name pocket-id-frontend
|
pm2 start --name pocket-id-frontend --node-args="--env-file .env" build/index.js
|
||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
# Optional: Start Caddy (You can use any other reverse proxy)
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ const config: Config = {
|
|||||||
"Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.",
|
"Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.",
|
||||||
favicon: "img/pocket-id.png",
|
favicon: "img/pocket-id.png",
|
||||||
|
|
||||||
url: "https://stonith404.github.io",
|
url: "https://docs.pocket-id.org",
|
||||||
baseUrl: "/pocket-id/",
|
baseUrl: "/",
|
||||||
organizationName: "stonith404",
|
organizationName: "stonith404",
|
||||||
projectName: "pocket-id",
|
projectName: "pocket-id",
|
||||||
|
|
||||||
@@ -47,6 +47,12 @@ const config: Config = {
|
|||||||
src: "img/pocket-id.png",
|
src: "img/pocket-id.png",
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
|
// Version gets replaced by the version-label.ts script
|
||||||
|
{
|
||||||
|
to: "#version",
|
||||||
|
label: " ",
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "https://github.com/stonith404/pocket-id",
|
href: "https://github.com/stonith404/pocket-id",
|
||||||
label: "GitHub",
|
label: "GitHub",
|
||||||
@@ -59,6 +65,7 @@ const config: Config = {
|
|||||||
darkTheme: prismThemes.dracula,
|
darkTheme: prismThemes.dracula,
|
||||||
},
|
},
|
||||||
} satisfies Preset.ThemeConfig,
|
} satisfies Preset.ThemeConfig,
|
||||||
};
|
|
||||||
|
|
||||||
|
clientModules: [require.resolve("./src/version-label.ts")],
|
||||||
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -59,12 +59,22 @@ const sidebars: SidebarsConfig = {
|
|||||||
slug: "client-examples",
|
slug: "client-examples",
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
"client-examples/hoarder",
|
|
||||||
"client-examples/jellyfin",
|
|
||||||
"client-examples/vikunja",
|
|
||||||
"client-examples/open-webui",
|
|
||||||
"client-examples/semaphore-ui",
|
|
||||||
"client-examples/cloudflare-zero-trust",
|
"client-examples/cloudflare-zero-trust",
|
||||||
|
"client-examples/freshrss",
|
||||||
|
"client-examples/grist",
|
||||||
|
"client-examples/headscale",
|
||||||
|
"client-examples/hoarder",
|
||||||
|
"client-examples/immich",
|
||||||
|
"client-examples/jellyfin",
|
||||||
|
"client-examples/netbox",
|
||||||
|
"client-examples/open-webui",
|
||||||
|
"client-examples/pgadmin",
|
||||||
|
"client-examples/portainer",
|
||||||
|
"client-examples/proxmox",
|
||||||
|
"client-examples/semaphore-ui",
|
||||||
|
"client-examples/vikunja",
|
||||||
|
"client-examples/gitea",
|
||||||
|
"client-examples/memos",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -82,15 +92,16 @@ const sidebars: SidebarsConfig = {
|
|||||||
label: "Helping Out",
|
label: "Helping Out",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: "doc",
|
type: "link",
|
||||||
id: "help-out/contribute",
|
label: "Contributing",
|
||||||
|
href: "https://github.com/stonith404/pocket-id/blob/main/CONTRIBUTING.md",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
label: "Demo",
|
label: "Demo",
|
||||||
href: "https://pocket-id.eliasschneider.com/",
|
href: "https://demo.pocket-id.org",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <Redirect to="/pocket-id/introduction" />;
|
return <Redirect to="/introduction" />;
|
||||||
}
|
}
|
||||||
|
|||||||
23
docs/src/version-label.ts
Normal file
23
docs/src/version-label.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
|
||||||
|
|
||||||
|
if (ExecutionEnvironment.canUseDOM) {
|
||||||
|
function readVersionFile() {
|
||||||
|
return fetch(
|
||||||
|
"https://raw.githubusercontent.com/stonith404/pocket-id/refs/heads/main/.version"
|
||||||
|
)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.catch((error) => `Error reading version file: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersion() {
|
||||||
|
readVersionFile()
|
||||||
|
.then((version) => {
|
||||||
|
const versionLabels = document.querySelectorAll('[href="#version"]');
|
||||||
|
versionLabels.forEach((label) => {
|
||||||
|
(label as HTMLElement).innerText = `v${version}`;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error fetching version:", error));
|
||||||
|
}
|
||||||
|
window.addEventListener("load", getVersion);
|
||||||
|
}
|
||||||
1
docs/static/CNAME
vendored
Normal file
1
docs/static/CNAME
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
docs.pocket-id.org
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
PUBLIC_APP_URL=http://localhost
|
PUBLIC_APP_URL=http://localhost
|
||||||
|
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
|
||||||
INTERNAL_BACKEND_URL=http://localhost:8080
|
INTERNAL_BACKEND_URL=http://localhost:8080
|
||||||
|
|||||||
10001
frontend/package-lock.json
generated
10001
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.27.0",
|
"version": "0.28.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3000",
|
"dev": "vite dev --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -11,48 +12,44 @@
|
|||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.48.1",
|
|
||||||
"@sveltejs/adapter-auto": "^3.3.0",
|
|
||||||
"@sveltejs/adapter-node": "^5.2.8",
|
|
||||||
"@sveltejs/kit": "^2.7.2",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
||||||
"@types/eslint": "^9.6.1",
|
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
|
||||||
"@types/node": "^22.7.9",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"cbor-js": "^0.1.0",
|
|
||||||
"eslint": "^9.13.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-svelte": "^2.46.0",
|
|
||||||
"globals": "^15.11.0",
|
|
||||||
"postcss": "^8.4.47",
|
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"prettier-plugin-svelte": "^3.2.7",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
|
||||||
"svelte": "^5.0.5",
|
|
||||||
"svelte-check": "^4.0.5",
|
|
||||||
"tailwindcss": "^3.4.14",
|
|
||||||
"tslib": "^2.8.0",
|
|
||||||
"typescript": "^5.6.3",
|
|
||||||
"typescript-eslint": "^8.11.0",
|
|
||||||
"vite": "^5.4.10"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"axios": "^1.7.7",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"bits-ui": "^0.21.16",
|
"axios": "^1.7.9",
|
||||||
|
"bits-ui": "^0.22.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jose": "^5.9.6",
|
||||||
"lucide-svelte": "^0.453.0",
|
"lucide-svelte": "^0.474.0",
|
||||||
"mode-watcher": "^0.4.1",
|
"mode-watcher": "^0.5.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"sveltekit-superforms": "^2.20.0",
|
"sveltekit-superforms": "^2.23.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwind-variants": "^0.2.1",
|
"tailwind-variants": "^0.3.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.50.0",
|
||||||
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
|
"@sveltejs/kit": "^2.16.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
|
"@types/eslint": "^9.6.1",
|
||||||
|
"@types/node": "^22.10.10",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"svelte": "^5.19.3",
|
||||||
|
"svelte-check": "^4.1.4",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.21.0",
|
||||||
|
"vite": "^6.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
@config '../tailwind.config.ts';
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
@@ -77,6 +77,10 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button{
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Playfair Display';
|
font-family: 'Playfair Display';
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import jwt from 'jsonwebtoken';
|
import { decodeJwt } from 'jose';
|
||||||
|
|
||||||
// Workaround so that we can also import this environment variable into client-side code
|
// Workaround so that we can also import this environment variable into client-side code
|
||||||
// If we would directly import $env/dynamic/private into the api-service.ts file, it would throw an error
|
// If we would directly import $env/dynamic/private into the api-service.ts file, it would throw an error
|
||||||
@@ -9,18 +10,7 @@ import jwt from 'jsonwebtoken';
|
|||||||
process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080';
|
process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080';
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const accessToken = event.cookies.get('access_token');
|
const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
|
|
||||||
let isSignedIn: boolean = false;
|
|
||||||
let isAdmin: boolean = false;
|
|
||||||
|
|
||||||
if (accessToken) {
|
|
||||||
const jwtPayload = jwt.decode(accessToken, { json: true });
|
|
||||||
if (jwtPayload?.exp && jwtPayload.exp * 1000 > Date.now()) {
|
|
||||||
isSignedIn = true;
|
|
||||||
isAdmin = jwtPayload?.isAdmin || false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.url.pathname.startsWith('/settings') && !event.url.pathname.startsWith('/login')) {
|
if (event.url.pathname.startsWith('/settings') && !event.url.pathname.startsWith('/login')) {
|
||||||
if (!isSignedIn) {
|
if (!isSignedIn) {
|
||||||
@@ -65,3 +55,18 @@ export const handleError: HandleServerError = async ({ error, message, status })
|
|||||||
status
|
status
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function verifyJwt(accessToken: string | undefined) {
|
||||||
|
let isSignedIn = false;
|
||||||
|
let isAdmin = false;
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
const jwtPayload = decodeJwt<{ isAdmin: boolean }>(accessToken);
|
||||||
|
if (jwtPayload?.exp && jwtPayload.exp * 1000 > Date.now()) {
|
||||||
|
isSignedIn = true;
|
||||||
|
isAdmin = jwtPayload?.isAdmin || false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isSignedIn, isAdmin };
|
||||||
|
}
|
||||||
|
|||||||
75
frontend/src/lib/components/collapsible-card.svelte
Normal file
75
frontend/src/lib/components/collapsible-card.svelte
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils/style';
|
||||||
|
import { LucideChevronDown } from 'lucide-svelte';
|
||||||
|
import { onMount, type Snippet } from 'svelte';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import * as Card from './ui/card';
|
||||||
|
|
||||||
|
let {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
defaultExpanded = false,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let expanded = $state(defaultExpanded);
|
||||||
|
|
||||||
|
function loadExpandedState() {
|
||||||
|
const state = JSON.parse(localStorage.getItem('collapsible-cards-expanded') || '{}');
|
||||||
|
expanded = state[id] || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveExpandedState() {
|
||||||
|
const state = JSON.parse(localStorage.getItem('collapsible-cards-expanded') || '{}');
|
||||||
|
state[id] = expanded;
|
||||||
|
localStorage.setItem('collapsible-cards-expanded', JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpanded() {
|
||||||
|
expanded = !expanded;
|
||||||
|
saveExpandedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (defaultExpanded) {
|
||||||
|
saveExpandedState();
|
||||||
|
}
|
||||||
|
loadExpandedState();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="cursor-pointer" onclick={toggleExpanded}>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Card.Title>{title}</Card.Title>
|
||||||
|
{#if description}
|
||||||
|
<Card.Description>{description}</Card.Description>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label="Expand card">
|
||||||
|
<LucideChevronDown
|
||||||
|
class={cn(
|
||||||
|
'h-5 w-5 transition-transform duration-200',
|
||||||
|
expanded && 'rotate-180 transform'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
{#if expanded}
|
||||||
|
<div transition:slide={{ duration: 200 }}>
|
||||||
|
<Card.Content>
|
||||||
|
{@render children()}
|
||||||
|
</Card.Content>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card.Root>
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class={cn('text-sm text-muted-foreground', className)} {...$$restProps}>
|
<p class={cn('text-sm text-muted-foreground mt-1', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
class={cn(
|
class={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
'relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||||
inset && 'pl-8',
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground',
|
'flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground',
|
||||||
inset && 'pl-8',
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
{disabled}
|
{disabled}
|
||||||
{label}
|
{label}
|
||||||
class={cn(
|
class={cn(
|
||||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
'relative flex w-full select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
2
frontend/src/lib/constants.ts
Normal file
2
frontend/src/lib/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const HTTPS_ENABLED = process.env.PUBLIC_APP_URL?.startsWith('https://') ?? false;
|
||||||
|
export const ACCESS_TOKEN_COOKIE_NAME = HTTPS_ENABLED ? '__Host-access_token' : 'access_token';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { version as currentVersion } from '$app/environment';
|
import { version as currentVersion } from '$app/environment';
|
||||||
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
|
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
|
||||||
import axios from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import APIService from './api-service';
|
import APIService from './api-service';
|
||||||
|
|
||||||
export default class AppConfigService extends APIService {
|
export default class AppConfigService extends APIService {
|
||||||
@@ -56,12 +56,23 @@ export default class AppConfigService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getVersionInformation() {
|
async getVersionInformation() {
|
||||||
const response = (
|
const response = await axios
|
||||||
await axios.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest')
|
.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest')
|
||||||
).data;
|
.then((res) => res.data)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(
|
||||||
|
'Failed to fetch version information',
|
||||||
|
e instanceof AxiosError && e.response ? e.response.data.message : e
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const newestVersion = response.tag_name.replace('v', '');
|
let newestVersion: string | null = null;
|
||||||
const isUpToDate = newestVersion === currentVersion;
|
let isUpToDate: boolean | null = null;
|
||||||
|
if (response) {
|
||||||
|
newestVersion = response.tag_name.replace('v', '');
|
||||||
|
isUpToDate = newestVersion === currentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isUpToDate,
|
isUpToDate,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { AuthorizeResponse, OidcClient, OidcClientCreate } from '$lib/types/oidc.type';
|
import type {
|
||||||
|
AuthorizeResponse,
|
||||||
|
OidcClient,
|
||||||
|
OidcClientCreate,
|
||||||
|
OidcClientWithAllowedUserGroups
|
||||||
|
} from '$lib/types/oidc.type';
|
||||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||||
import APIService from './api-service';
|
import APIService from './api-service';
|
||||||
|
|
||||||
@@ -23,24 +28,13 @@ class OidcService extends APIService {
|
|||||||
return res.data as AuthorizeResponse;
|
return res.data as AuthorizeResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authorizeNewClient(
|
async isAuthorizationRequired(clientId: string, scope: string) {
|
||||||
clientId: string,
|
const res = await this.api.post('/oidc/authorization-required', {
|
||||||
scope: string,
|
|
||||||
callbackURL: string,
|
|
||||||
nonce?: string,
|
|
||||||
codeChallenge?: string,
|
|
||||||
codeChallengeMethod?: string
|
|
||||||
) {
|
|
||||||
const res = await this.api.post('/oidc/authorize/new-client', {
|
|
||||||
scope,
|
scope,
|
||||||
nonce,
|
clientId
|
||||||
callbackURL,
|
|
||||||
clientId,
|
|
||||||
codeChallenge,
|
|
||||||
codeChallengeMethod
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.data as AuthorizeResponse;
|
return res.data.authorizationRequired as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listClients(options?: SearchPaginationSortRequest) {
|
async listClients(options?: SearchPaginationSortRequest) {
|
||||||
@@ -59,7 +53,7 @@ class OidcService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(id: string) {
|
async getClient(id: string) {
|
||||||
return (await this.api.get(`/oidc/clients/${id}`)).data as OidcClient;
|
return (await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateClient(id: string, client: OidcClientCreate) {
|
async updateClient(id: string, client: OidcClientCreate) {
|
||||||
@@ -88,6 +82,11 @@ class OidcService extends APIService {
|
|||||||
async createClientSecret(id: string) {
|
async createClientSecret(id: string) {
|
||||||
return (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
|
return (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateAllowedUserGroups(id: string, userGroupIds: string[]) {
|
||||||
|
const res = await this.api.put(`/oidc/clients/${id}/allowed-user-groups`, { userGroupIds });
|
||||||
|
return res.data as OidcClientWithAllowedUserGroups;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OidcService;
|
export default OidcService;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Passkey } from '$lib/types/passkey.type';
|
import type { Passkey } from '$lib/types/passkey.type';
|
||||||
import type { User } from '$lib/types/user.type';
|
import type { User } from '$lib/types/user.type';
|
||||||
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/types';
|
|
||||||
import APIService from './api-service';
|
import APIService from './api-service';
|
||||||
import userStore from '$lib/stores/user-store';
|
import userStore from '$lib/stores/user-store';
|
||||||
|
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/browser';
|
||||||
|
|
||||||
class WebAuthnService extends APIService {
|
class WebAuthnService extends APIService {
|
||||||
async getRegistrationOptions() {
|
async getRegistrationOptions() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type AppConfig = {
|
|||||||
appName: string;
|
appName: string;
|
||||||
allowOwnAccountEdit: boolean;
|
allowOwnAccountEdit: boolean;
|
||||||
emailOneTimeAccessEnabled: boolean;
|
emailOneTimeAccessEnabled: boolean;
|
||||||
|
ldapEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AllAppConfig = AppConfig & {
|
export type AllAppConfig = AppConfig & {
|
||||||
@@ -18,7 +19,6 @@ export type AllAppConfig = AppConfig & {
|
|||||||
smtpSkipCertVerify: boolean;
|
smtpSkipCertVerify: boolean;
|
||||||
emailLoginNotificationEnabled: boolean;
|
emailLoginNotificationEnabled: boolean;
|
||||||
// LDAP
|
// LDAP
|
||||||
ldapEnabled: boolean;
|
|
||||||
ldapUrl: string;
|
ldapUrl: string;
|
||||||
ldapBindDn: string;
|
ldapBindDn: string;
|
||||||
ldapBindPassword: string;
|
ldapBindPassword: string;
|
||||||
@@ -41,7 +41,7 @@ export type AppConfigRawResponse = {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
export type AppVersionInformation = {
|
export type AppVersionInformation = {
|
||||||
isUpToDate: boolean;
|
isUpToDate: boolean | null;
|
||||||
newestVersion: string;
|
newestVersion: string | null;
|
||||||
currentVersion: string;
|
currentVersion: string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { UserGroup } from './user-group.type';
|
||||||
|
|
||||||
export type OidcClient = {
|
export type OidcClient = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -8,6 +10,10 @@ export type OidcClient = {
|
|||||||
pkceEnabled: boolean;
|
pkceEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OidcClientWithAllowedUserGroups = OidcClient & {
|
||||||
|
allowedUserGroups: UserGroup[];
|
||||||
|
};
|
||||||
|
|
||||||
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
||||||
|
|
||||||
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||||
const userService = new UserService(cookies.get('access_token'));
|
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const appConfigService = new AppConfigService(cookies.get('access_token'));
|
const appConfigService = new AppConfigService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
|
|
||||||
const user = await userService
|
const user = await userService
|
||||||
.getCurrent()
|
.getCurrent()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import OidcService from '$lib/services/oidc-service';
|
import OidcService from '$lib/services/oidc-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ url, cookies }) => {
|
export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||||
const clientId = url.searchParams.get('client_id');
|
const clientId = url.searchParams.get('client_id');
|
||||||
const oidcService = new OidcService(cookies.get('access_token'));
|
const oidcService = new OidcService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
|
|
||||||
const client = await oidcService.getClient(clientId!);
|
const client = await oidcService.getClient(clientId!);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
let success = false;
|
let success = false;
|
||||||
let errorMessage: string | null = null;
|
let errorMessage: string | null = null;
|
||||||
let authorizationRequired = false;
|
let authorizationRequired = false;
|
||||||
|
let authorizationConfirmed = false;
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
let { scope, nonce, client, state, callbackURL, codeChallenge, codeChallengeMethod } = data;
|
let { scope, nonce, client, state, callbackURL, codeChallenge, codeChallengeMethod } = data;
|
||||||
@@ -40,7 +41,17 @@
|
|||||||
if (!$userStore?.id) {
|
if (!$userStore?.id) {
|
||||||
const loginOptions = await webauthnService.getLoginOptions();
|
const loginOptions = await webauthnService.getLoginOptions();
|
||||||
const authResponse = await startAuthentication(loginOptions);
|
const authResponse = await startAuthentication(loginOptions);
|
||||||
await webauthnService.finishLogin(authResponse);
|
const user = await webauthnService.finishLogin(authResponse);
|
||||||
|
userStore.setUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorizationConfirmed) {
|
||||||
|
authorizationRequired = await oidService.isAuthorizationRequired(client!.id, scope);
|
||||||
|
if (authorizationRequired) {
|
||||||
|
isLoading = false;
|
||||||
|
authorizationConfirmed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await oidService
|
await oidService
|
||||||
@@ -49,7 +60,7 @@
|
|||||||
onSuccess(code, callbackURL);
|
onSuccess(code, callbackURL);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof AxiosError && e.response?.status === 403) {
|
if (e instanceof AxiosError && e.response?.data.error === 'Missing authorization') {
|
||||||
authorizationRequired = true;
|
authorizationRequired = true;
|
||||||
} else {
|
} else {
|
||||||
errorMessage = getWebauthnErrorMessage(e);
|
errorMessage = getWebauthnErrorMessage(e);
|
||||||
@@ -58,27 +69,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authorizeNewClient() {
|
|
||||||
isLoading = true;
|
|
||||||
try {
|
|
||||||
await oidService
|
|
||||||
.authorizeNewClient(
|
|
||||||
client!.id,
|
|
||||||
scope,
|
|
||||||
callbackURL,
|
|
||||||
nonce,
|
|
||||||
codeChallenge,
|
|
||||||
codeChallengeMethod
|
|
||||||
)
|
|
||||||
.then(async ({ code, callbackURL }) => {
|
|
||||||
onSuccess(code, callbackURL);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
errorMessage = getWebauthnErrorMessage(e);
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSuccess(code: string, callbackURL: string) {
|
function onSuccess(code: string, callbackURL: string) {
|
||||||
success = true;
|
success = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -100,14 +90,14 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<SignInWrapper showEmailOneTimeAccessButton={$appConfigStore.emailOneTimeAccessEnabled}>
|
<SignInWrapper showEmailOneTimeAccessButton={$appConfigStore.emailOneTimeAccessEnabled}>
|
||||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||||
<h1 class="mt-5 font-playfair text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<p class="mb-10 mt-2 text-muted-foreground">
|
<p class="text-muted-foreground mb-10 mt-2">
|
||||||
{errorMessage}. Please try again.
|
{errorMessage}.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !authorizationRequired && !errorMessage}
|
{#if !authorizationRequired && !errorMessage}
|
||||||
<p class="mb-10 mt-2 text-muted-foreground">
|
<p class="text-muted-foreground mb-10 mt-2">
|
||||||
Do you want to sign in to <b>{client.name}</b> with your
|
Do you want to sign in to <b>{client.name}</b> with your
|
||||||
<b>{$appConfigStore.appName}</b> account?
|
<b>{$appConfigStore.appName}</b> account?
|
||||||
</p>
|
</p>
|
||||||
@@ -115,7 +105,7 @@
|
|||||||
<div transition:slide={{ duration: 300 }}>
|
<div transition:slide={{ duration: 300 }}>
|
||||||
<Card.Root class="mb-10 mt-6">
|
<Card.Root class="mb-10 mt-6">
|
||||||
<Card.Header class="pb-5">
|
<Card.Header class="pb-5">
|
||||||
<p class="text-start text-muted-foreground">
|
<p class="text-muted-foreground text-start">
|
||||||
<b>{client.name}</b> wants to access the following information:
|
<b>{client.name}</b> wants to access the following information:
|
||||||
</p>
|
</p>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
@@ -146,13 +136,7 @@
|
|||||||
<div class="flex w-full justify-stretch gap-2">
|
<div class="flex w-full justify-stretch gap-2">
|
||||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">Cancel</Button>
|
<Button onclick={() => history.back()} class="w-full" variant="secondary">Cancel</Button>
|
||||||
{#if !errorMessage}
|
{#if !errorMessage}
|
||||||
<Button
|
<Button class="w-full" {isLoading} on:click={authorize}>Sign in</Button>
|
||||||
class="w-full"
|
|
||||||
{isLoading}
|
|
||||||
on:click={authorizationRequired ? authorizeNewClient : authorize}
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Button class="w-full" on:click={() => (errorMessage = null)}>Try again</Button>
|
<Button class="w-full" on:click={() => (errorMessage = null)}>Try again</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-center gap-3">
|
<div class="flex justify-center gap-3">
|
||||||
<div
|
<div
|
||||||
class=" rounded-2xl bg-muted p-3 transition-transform duration-500 ease-in {success || error
|
class=" bg-muted transition-translate rounded-2xl p-3 duration-500 ease-in {success || error
|
||||||
? 'translate-x-[108px]'
|
? 'translate-x-[108px]'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
@@ -38,10 +38,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConnectArrow
|
<ConnectArrow
|
||||||
class="arrow-fade-out h-w-32 w-32 {success || error ? 'opacity-0' : 'opacity-100'}"
|
class="h-w-32 w-32 transition-opacity duration-500 {success || error
|
||||||
|
? 'opacity-0'
|
||||||
|
: 'opacity-100 delay-300'}"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="rounded-2xl p-3 [transition:transform_500ms_ease-in,background-color_200ms] {success ||
|
class="rounded-2xl p-3 [transition:translate_500ms_ease-in,background-color_200ms] {success ||
|
||||||
error
|
error
|
||||||
? '-translate-x-[108px]'
|
? '-translate-x-[108px]'
|
||||||
: ''} {animationDone ? (success ? 'bg-green-200' : 'bg-red-200') : 'bg-muted'}"
|
: ''} {animationDone ? (success ? 'bg-green-200' : 'bg-red-200') : 'bg-muted'}"
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export const load: LayoutServerLoad = async () => {
|
|||||||
|
|
||||||
if (!versionInformation || cacheExpired) {
|
if (!versionInformation || cacheExpired) {
|
||||||
versionInformation = await appConfigService.getVersionInformation();
|
versionInformation = await appConfigService.getVersionInformation();
|
||||||
|
if (versionInformation.newestVersion == null) {
|
||||||
|
console.error('Failed to fetch version information. Trying again in 3 hours.');
|
||||||
|
}
|
||||||
versionInformationLastUpdated = Date.now();
|
versionInformationLastUpdated = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
{#if $userStore?.isAdmin && !versionInformation.isUpToDate}
|
{#if $userStore?.isAdmin && versionInformation.isUpToDate === false}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/stonith404/pocket-id/releases/latest"
|
href="https://github.com/stonith404/pocket-id/releases/latest"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import WebAuthnService from '$lib/services/webauthn-service';
|
import WebAuthnService from '$lib/services/webauthn-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ cookies }) => {
|
export const load: PageServerLoad = async ({ cookies }) => {
|
||||||
const webauthnService = new WebAuthnService(cookies.get('access_token'));
|
const webauthnService = new WebAuthnService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const userService = new UserService(cookies.get('access_token'));
|
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const account = await userService.getCurrent();
|
const account = await userService.getCurrent();
|
||||||
const passkeys = await webauthnService.listCredentials();
|
const passkeys = await webauthnService.listCredentials();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<fieldset disabled={!$appConfigStore.allowOwnAccountEdit || !!account.ldapId}>
|
<fieldset disabled={!$appConfigStore.allowOwnAccountEdit || (!!account.ldapId && $appConfigStore.ldapEnabled)}>
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>Account Details</Card.Title>
|
<Card.Title>Account Details</Card.Title>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ cookies }) => {
|
export const load: PageServerLoad = async ({ cookies }) => {
|
||||||
const appConfigService = new AppConfigService(cookies.get('access_token'));
|
const appConfigService = new AppConfigService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const appConfig = await appConfigService.list(true);
|
const appConfig = await appConfigService.list(true);
|
||||||
return { appConfig };
|
return { appConfig };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
@@ -55,45 +55,27 @@
|
|||||||
<title>Application Configuration</title>
|
<title>Application Configuration</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Card.Root>
|
<CollapsibleCard id="application-configuration-general" title="General" defaultExpanded>
|
||||||
<Card.Header>
|
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||||
<Card.Title>General</Card.Title>
|
</CollapsibleCard>
|
||||||
</Card.Header>
|
|
||||||
<Card.Content>
|
|
||||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<Card.Root>
|
<CollapsibleCard
|
||||||
<Card.Header>
|
id="application-configuration-email"
|
||||||
<Card.Title>Email</Card.Title>
|
title="Email"
|
||||||
<Card.Description>
|
description="Enable email notifications to alert users when a login is detected from a new device or
|
||||||
Enable email notifications to alert users when a login is detected from a new device or
|
location."
|
||||||
location.
|
>
|
||||||
</Card.Description>
|
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||||
</Card.Header>
|
</CollapsibleCard>
|
||||||
<Card.Content>
|
|
||||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<Card.Root>
|
<CollapsibleCard
|
||||||
<Card.Header>
|
id="application-configuration-ldap"
|
||||||
<Card.Title>LDAP</Card.Title>
|
title="LDAP"
|
||||||
<Card.Description>
|
description="Configure LDAP settings to sync users and groups from an LDAP server."
|
||||||
Configure LDAP settings to sync users and groups from an LDAP server.
|
>
|
||||||
</Card.Description>
|
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||||
</Card.Header>
|
</CollapsibleCard>
|
||||||
<Card.Content>
|
|
||||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<Card.Root>
|
<CollapsibleCard id="application-configuration-images" title="Images">
|
||||||
<Card.Header>
|
<UpdateApplicationImages callback={updateImages} />
|
||||||
<Card.Title>Images</Card.Title>
|
</CollapsibleCard>
|
||||||
</Card.Header>
|
|
||||||
<Card.Content>
|
|
||||||
<UpdateApplicationImages callback={updateImages} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
isSendingTestEmail = true;
|
isSendingTestEmail = true;
|
||||||
await appConfigService
|
await appConfigService
|
||||||
.sendTestEmail()
|
.sendTestEmail()
|
||||||
.then(() => toast.success('Test email sent successfully to your Email address.'))
|
.then(() => toast.success('Test email sent successfully to your email address.'))
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
toast.error('Failed to send test email. Check the server logs for more information.')
|
toast.error('Failed to send test email. Check the server logs for more information.')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import OIDCService from '$lib/services/oidc-service';
|
import OIDCService from '$lib/services/oidc-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ cookies }) => {
|
export const load: PageServerLoad = async ({ cookies }) => {
|
||||||
const oidcService = new OIDCService(cookies.get('access_token'));
|
const oidcService = new OIDCService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const clients = await oidcService.listClients();
|
const clients = await oidcService.listClients();
|
||||||
return clients;
|
return clients;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import OidcService from '$lib/services/oidc-service';
|
import OidcService from '$lib/services/oidc-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
const oidcService = new OidcService(cookies.get('access_token'));
|
const oidcService = new OidcService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
return await oidcService.getClient(params.id);
|
return await oidcService.getClient(params.id);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { beforeNavigate } from '$app/navigation';
|
import { beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import OidcService from '$lib/services/oidc-service';
|
import OidcService from '$lib/services/oidc-service';
|
||||||
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import clientSecretStore from '$lib/stores/client-secret-store';
|
import clientSecretStore from '$lib/stores/client-secret-store';
|
||||||
import type { OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
import type { OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
@@ -14,12 +16,17 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import OidcForm from '../oidc-client-form.svelte';
|
import OidcForm from '../oidc-client-form.svelte';
|
||||||
|
import UserGroupSelection from '../user-group-selection.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let client = $state(data);
|
let client = $state({
|
||||||
|
...data,
|
||||||
|
allowedUserGroupIds: data.allowedUserGroups.map((g) => g.id)
|
||||||
|
});
|
||||||
let showAllDetails = $state(false);
|
let showAllDetails = $state(false);
|
||||||
|
|
||||||
const oidcService = new OidcService();
|
const oidcService = new OidcService();
|
||||||
|
const userGroupService = new UserGroupService();
|
||||||
|
|
||||||
const setupDetails = $state({
|
const setupDetails = $state({
|
||||||
'Authorization URL': `https://${$page.url.hostname}/authorize`,
|
'Authorization URL': `https://${$page.url.hostname}/authorize`,
|
||||||
@@ -74,6 +81,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateUserGroupClients(allowedGroups: string[]) {
|
||||||
|
await oidcService
|
||||||
|
.updateAllowedUserGroups(client.id, allowedGroups)
|
||||||
|
.then(() => {
|
||||||
|
toast.success('Allowed user groups updated successfully');
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
axiosErrorToast(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
beforeNavigate(() => {
|
beforeNavigate(() => {
|
||||||
clientSecretStore.clear();
|
clientSecretStore.clear();
|
||||||
});
|
});
|
||||||
@@ -84,7 +102,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a class="flex text-sm text-muted-foreground" href="/settings/admin/oidc-clients"
|
<a class="text-muted-foreground flex text-sm" href="/settings/admin/oidc-clients"
|
||||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,7 +115,7 @@
|
|||||||
<div class="mb-2 flex">
|
<div class="mb-2 flex">
|
||||||
<Label class="mb-0 w-44">Client ID</Label>
|
<Label class="mb-0 w-44">Client ID</Label>
|
||||||
<CopyToClipboard value={client.id}>
|
<CopyToClipboard value={client.id}>
|
||||||
<span class="text-sm text-muted-foreground" data-testid="client-id"> {client.id}</span>
|
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</div>
|
</div>
|
||||||
{#if !client.isPublic}
|
{#if !client.isPublic}
|
||||||
@@ -105,12 +123,12 @@
|
|||||||
<Label class="w-44">Client secret</Label>
|
<Label class="w-44">Client secret</Label>
|
||||||
{#if $clientSecretStore}
|
{#if $clientSecretStore}
|
||||||
<CopyToClipboard value={$clientSecretStore}>
|
<CopyToClipboard value={$clientSecretStore}>
|
||||||
<span class="text-sm text-muted-foreground" data-testid="client-secret">
|
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||||
{$clientSecretStore}
|
{$clientSecretStore}
|
||||||
</span>
|
</span>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-sm text-muted-foreground" data-testid="client-secret"
|
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||||
>••••••••••••••••••••••••••••••••</span
|
>••••••••••••••••••••••••••••••••</span
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -129,7 +147,7 @@
|
|||||||
<div class="mb-5 flex">
|
<div class="mb-5 flex">
|
||||||
<Label class="mb-0 w-44">{key}</Label>
|
<Label class="mb-0 w-44">{key}</Label>
|
||||||
<CopyToClipboard {value}>
|
<CopyToClipboard {value}>
|
||||||
<span class="text-sm text-muted-foreground">{value}</span>
|
<span class="text-muted-foreground text-sm">{value}</span>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -151,3 +169,15 @@
|
|||||||
<OidcForm existingClient={client} callback={updateClient} />
|
<OidcForm existingClient={client} callback={updateClient} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
<CollapsibleCard
|
||||||
|
id="allowed-user-groups"
|
||||||
|
title="Allowed User Groups"
|
||||||
|
description="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."
|
||||||
|
>
|
||||||
|
{#await userGroupService.list() then groups}
|
||||||
|
<UserGroupSelection {groups} bind:selectedGroupIds={client.allowedUserGroupIds} />
|
||||||
|
{/await}
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<Button on:click={() => updateUserGroupClients(client.allowedUserGroupIds)}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</CollapsibleCard>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={onSubmit}>
|
<form onsubmit={onSubmit}>
|
||||||
<div class="grid grid-cols-2 gap-3 sm:flex-row">
|
<div class="grid grid-cols-2 gap-x-3 gap-y-7 sm:flex-row">
|
||||||
<FormInput label="Name" class="w-full" bind:input={$inputs.name} />
|
<FormInput label="Name" class="w-full" bind:input={$inputs.name} />
|
||||||
<OidcCallbackUrlInput
|
<OidcCallbackUrlInput
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||||
|
import * as Table from '$lib/components/ui/table';
|
||||||
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
|
import type { OidcClient } from '$lib/types/oidc.type';
|
||||||
|
import type { Paginated } from '$lib/types/pagination.type';
|
||||||
|
import type { UserGroup } from '$lib/types/user-group.type';
|
||||||
|
|
||||||
|
let {
|
||||||
|
groups: initialGroups,
|
||||||
|
selectionDisabled = false,
|
||||||
|
selectedGroupIds = $bindable()
|
||||||
|
}: {
|
||||||
|
groups: Paginated<UserGroup>;
|
||||||
|
selectionDisabled?: boolean;
|
||||||
|
selectedGroupIds: string[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const userGroupService = new UserGroupService();
|
||||||
|
|
||||||
|
let groups = $state(initialGroups);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AdvancedTable
|
||||||
|
items={groups}
|
||||||
|
onRefresh={async (o) => (groups = await userGroupService.list(o))}
|
||||||
|
columns={[{ label: 'Name', sortColumn: 'name' }]}
|
||||||
|
bind:selectedIds={selectedGroupIds}
|
||||||
|
{selectionDisabled}
|
||||||
|
>
|
||||||
|
{#snippet rows({ item })}
|
||||||
|
<Table.Cell>{item.name}</Table.Cell>
|
||||||
|
{/snippet}
|
||||||
|
</AdvancedTable>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ cookies }) => {
|
export const load: PageServerLoad = async ({ cookies }) => {
|
||||||
const userGroupService = new UserGroupService(cookies.get('access_token'));
|
const userGroupService = new UserGroupService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const userGroups = await userGroupService.list();
|
const userGroups = await userGroupService.list();
|
||||||
return userGroups;
|
return userGroups;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
const userGroupService = new UserGroupService(cookies.get('access_token'));
|
const userGroupService = new UserGroupService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const userGroup = await userGroupService.get(params.id);
|
const userGroup = await userGroupService.get(params.id);
|
||||||
|
|
||||||
return { userGroup };
|
return { userGroup };
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
import CustomClaimsInput from '$lib/components/custom-claims-input.svelte';
|
import CustomClaimsInput from '$lib/components/custom-claims-input.svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import UserGroupForm from '../user-group-form.svelte';
|
import UserGroupForm from '../user-group-form.svelte';
|
||||||
import UserSelection from '../user-selection.svelte';
|
import UserSelection from '../user-selection.svelte';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let userGroup = $state({
|
let userGroup = $state({
|
||||||
@@ -60,7 +62,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<a class="flex text-sm text-muted-foreground" href="/settings/admin/user-groups"
|
<a class="text-muted-foreground flex text-sm" href="/settings/admin/user-groups"
|
||||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||||
>
|
>
|
||||||
{#if !!userGroup.ldapId}
|
{#if !!userGroup.ldapId}
|
||||||
@@ -88,30 +90,24 @@
|
|||||||
<UserSelection
|
<UserSelection
|
||||||
{users}
|
{users}
|
||||||
bind:selectedUserIds={userGroup.userIds}
|
bind:selectedUserIds={userGroup.userIds}
|
||||||
selectionDisabled={!!userGroup.ldapId}
|
selectionDisabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||||
/>
|
/>
|
||||||
{/await}
|
{/await}
|
||||||
<div class="mt-5 flex justify-end">
|
<div class="mt-5 flex justify-end">
|
||||||
<Button disabled={!!userGroup.ldapId} on:click={() => updateUserGroupUsers(userGroup.userIds)}
|
<Button disabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled} on:click={() => updateUserGroupUsers(userGroup.userIds)}
|
||||||
>Save</Button
|
>Save</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
<Card.Root>
|
<CollapsibleCard
|
||||||
<Card.Header>
|
id="user-group-custom-claims"
|
||||||
<Card.Title>Custom Claims</Card.Title>
|
title="Custom Claims"
|
||||||
<Card.Description>
|
description="Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts."
|
||||||
Custom claims are key-value pairs that can be used to store additional information about a
|
>
|
||||||
user. These claims will be included in the ID token if the scope "profile" is requested.
|
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
||||||
Custom claims defined on the user will be prioritized if there are conflicts.
|
<div class="mt-5 flex justify-end">
|
||||||
</Card.Description>
|
<Button onclick={updateCustomClaims} type="submit">Save</Button>
|
||||||
</Card.Header>
|
</div>
|
||||||
<Card.Content>
|
</CollapsibleCard>
|
||||||
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
|
||||||
<div class="mt-5 flex justify-end">
|
|
||||||
<Button onclick={updateCustomClaims} type="submit">Save</Button>
|
|
||||||
</div>
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { UserGroupCreate } from '$lib/types/user-group.type';
|
import type { UserGroupCreate } from '$lib/types/user-group.type';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let inputDisabled = $derived(!!existingUserGroup?.ldapId);
|
let inputDisabled = $derived(!!existingUserGroup?.ldapId && $appConfigStore.ldapEnabled);
|
||||||
let hasManualNameEdit = $state(!!existingUserGroup?.friendlyName);
|
let hasManualNameEdit = $state(!!existingUserGroup?.friendlyName);
|
||||||
|
|
||||||
const userGroup = {
|
const userGroup = {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user