Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d071694cd | ||
|
|
39e403d00f | ||
|
|
4e858420e9 | ||
|
|
2d78349b38 | ||
|
|
9ed2adb0f8 | ||
|
|
43790dc1be | ||
|
|
7fbc356d8d | ||
|
|
9b77e8b7c1 | ||
|
|
bea115866f | ||
|
|
626f87d592 | ||
|
|
0751540d7d | ||
|
|
7c04bda5b7 | ||
|
|
98add37390 | ||
|
|
3dda2e16e9 | ||
|
|
3a6fce5c4b | ||
|
|
07ee087c3d | ||
|
|
d66cf70d50 | ||
|
|
fb8cc0bb22 | ||
|
|
0bae7e4f53 | ||
|
|
974b7b3c34 | ||
|
|
15cde6ac66 | ||
|
|
e864d5dcbf | ||
|
|
c6ab2b252c | ||
|
|
7350e3486d | ||
|
|
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 | ||
|
|
440a9f1ba0 | ||
|
|
d02f4753f3 | ||
|
|
ede7d8fc15 | ||
|
|
e4e6c9b680 | ||
|
|
c12bf2955b | ||
|
|
c211d3fc67 | ||
|
|
d87eb416cd | ||
|
|
f7710f2988 | ||
|
|
72923bb86d | ||
|
|
6e44b5e367 | ||
|
|
8a1db0cb4a | ||
|
|
3f02d08109 | ||
|
|
715040ba04 | ||
|
|
a8b9d60a86 | ||
|
|
712ff396f4 | ||
|
|
090eca202d | ||
|
|
d4055af3f4 | ||
|
|
692ff70c91 | ||
|
|
d5dd118a3f | ||
|
|
06b90eddd6 | ||
|
|
e284e352e2 | ||
|
|
5101b14eec | ||
|
|
bc8f454ea1 | ||
|
|
fda08ac1cd | ||
|
|
05a98ebe87 | ||
|
|
6e3728ddc8 | ||
|
|
5c57beb4d7 | ||
|
|
2a984eeaf1 | ||
|
|
be6e25a167 | ||
|
|
888557171d | ||
|
|
4d337a20c5 | ||
|
|
69afd9ad9f | ||
|
|
fd69830c26 | ||
|
|
61d18a9d1b | ||
|
|
a649c4b4a5 | ||
|
|
82e475a923 | ||
|
|
2d31fc2cc9 | ||
|
|
adcf3ddc66 | ||
|
|
785200de61 | ||
|
|
ee885fbff5 | ||
|
|
333a1a18d5 | ||
|
|
1ff20caa3c | ||
|
|
f6f2736bba | ||
|
|
993330d932 | ||
|
|
204313aacf | ||
|
|
0729ce9e1a | ||
|
|
2d0bd8dcbf | ||
|
|
ff75322e7d | ||
|
|
daced661c4 | ||
|
|
0716c38fb8 | ||
|
|
789d9394a5 | ||
|
|
aeda512cb7 | ||
|
|
5480ab0f18 | ||
|
|
bad901ea2b | ||
|
|
34e35193f9 | ||
|
|
232c13b5ca | ||
|
|
9d20a98dbb | ||
|
|
e9d83dd6c3 | ||
|
|
3006bc9ef7 | ||
|
|
ae1e2f5e77 | ||
|
|
edce3d3371 | ||
|
|
9a8ec15678 | ||
|
|
62cdab2b59 | ||
|
|
f2bfc73158 | ||
|
|
a9f4dada32 | ||
|
|
f9fa2c6706 | ||
|
|
7d6b1d19e9 | ||
|
|
31a6b57ec1 | ||
|
|
f11ed44733 | ||
|
|
541481721f | ||
|
|
0e95e9c56f | ||
|
|
fcf08a4d89 | ||
|
|
0b4101ccce | ||
|
|
27ea1fc2d3 | ||
|
|
f637a89f57 | ||
|
|
058084ed64 | ||
|
|
9370292fe5 | ||
|
|
46eef1fcb7 | ||
|
|
e784093342 | ||
|
|
653d948f73 | ||
|
|
a1302ef7bf | ||
|
|
5f44fef85f | ||
|
|
3613ac261c | ||
|
|
760c8e83bb | ||
|
|
3f29325f45 | ||
|
|
aca2240a50 | ||
|
|
de45398903 | ||
|
|
3d3fb4d855 | ||
|
|
725388fcc7 | ||
|
|
ad1d3560f9 | ||
|
|
becfc0004a | ||
|
|
376d747616 | ||
|
|
5b9f4d7326 | ||
|
|
0de4b55dc4 | ||
|
|
78c88f5339 | ||
|
|
60e7dafa01 | ||
|
|
2ccabf835c | ||
|
|
590cb02f6c | ||
|
|
8c96ab9574 | ||
|
|
3484daf870 | ||
|
|
cfbc0d6d35 | ||
|
|
939601b6a4 | ||
|
|
b9daa5d757 | ||
|
|
8304065652 | ||
|
|
7bfc3f43a5 | ||
|
|
c056089c60 | ||
|
|
3350398abc | ||
|
|
0b0a6781ff | ||
|
|
735dc70d5f | ||
|
|
47e164b4b5 | ||
|
|
18c5103c20 | ||
|
|
5565f60d6d |
@@ -1,2 +1,6 @@
|
|||||||
|
# See the README for more information: https://github.com/pocket-id/pocket-id?tab=readme-ov-file#environment-variables
|
||||||
PUBLIC_APP_URL=http://localhost
|
PUBLIC_APP_URL=http://localhost
|
||||||
TRUST_PROXY=false
|
TRUST_PROXY=false
|
||||||
|
MAXMIND_LICENSE_KEY=
|
||||||
|
PUID=1000
|
||||||
|
PGID=1000
|
||||||
|
|||||||
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.**
|
||||||
|
|||||||
@@ -6,25 +6,42 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04 # Using an older version because of https://github.com/actions/runner-images/issues/11471
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},prefix=v
|
||||||
|
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker registry
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Download GeoLite2 City database
|
- name: 'Login to GitHub Container Registry'
|
||||||
run: MAXMIND_LICENSE_KEY=${{ secrets.MAXMIND_LICENSE_KEY }} sh scripts/download-ip-database.sh
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{github.repository_owner}}
|
||||||
|
password: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
@@ -32,6 +49,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: stonith404/pocket-id:latest,stonith404/pocket-id:${{ github.ref_name }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
133
.github/workflows/e2e-tests.yml
vendored
@@ -2,27 +2,55 @@ 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-and-test:
|
build:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and export
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
tags: pocket-id/pocket-id:test
|
||||||
|
outputs: type=docker,dest=/tmp/docker-image.tar
|
||||||
|
|
||||||
|
- name: Upload Docker image artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: docker-image
|
||||||
|
path: /tmp/docker-image.tar
|
||||||
|
|
||||||
|
test-sqlite:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
- name: Create dummy GeoLite2 City database
|
- name: Download Docker image artifact
|
||||||
run: touch ./backend/GeoLite2-City.mmdb
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
- name: Build Docker Image
|
name: docker-image
|
||||||
run: docker build -t stonith404/pocket-id .
|
path: /tmp
|
||||||
- name: Run Docker Container
|
- name: Load Docker Image
|
||||||
run: docker run -d --name pocket-id -p 80:80 --env-file .env.test stonith404/pocket-id
|
run: docker load -i /tmp/docker-image.tar
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
@@ -32,17 +60,94 @@ jobs:
|
|||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run Docker Container with Sqlite DB
|
||||||
|
run: |
|
||||||
|
docker run -d --name pocket-id-sqlite \
|
||||||
|
-p 80:80 \
|
||||||
|
-e APP_ENV=test \
|
||||||
|
pocket-id/pocket-id:test
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
|
|
||||||
- name: Get container logs
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
run: docker logs pocket-id
|
with:
|
||||||
|
name: playwright-report-sqlite
|
||||||
|
path: frontend/tests/.report
|
||||||
|
include-hidden-files: true
|
||||||
|
retention-days: 15
|
||||||
|
|
||||||
|
test-postgres:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Download Docker image artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: docker-image
|
||||||
|
path: /tmp
|
||||||
|
- name: Load Docker Image
|
||||||
|
run: docker load -i /tmp/docker-image.tar
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Create Docker network
|
||||||
|
run: docker network create pocket-id-network
|
||||||
|
|
||||||
|
- name: Start Postgres DB
|
||||||
|
run: |
|
||||||
|
docker run -d --name pocket-id-db \
|
||||||
|
--network pocket-id-network \
|
||||||
|
-e POSTGRES_USER=postgres \
|
||||||
|
-e POSTGRES_PASSWORD=postgres \
|
||||||
|
-e POSTGRES_DB=pocket-id \
|
||||||
|
-p 5432:5432 \
|
||||||
|
postgres:17
|
||||||
|
|
||||||
|
- name: Wait for Postgres to start
|
||||||
|
run: |
|
||||||
|
for i in {1..10}; do
|
||||||
|
if docker exec pocket-id-db pg_isready -U postgres; then
|
||||||
|
echo "Postgres is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting for Postgres..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Run Docker Container with Postgres DB
|
||||||
|
run: |
|
||||||
|
docker run -d --name pocket-id-postgres \
|
||||||
|
--network pocket-id-network \
|
||||||
|
-p 80:80 \
|
||||||
|
-e APP_ENV=test \
|
||||||
|
-e DB_PROVIDER=postgres \
|
||||||
|
-e POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@pocket-id-db:5432/pocket-id \
|
||||||
|
pocket-id/pocket-id:test
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npx playwright test
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report-postgres
|
||||||
path: frontend/tests/.output
|
path: frontend/tests/.report
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 15
|
retention-days: 15
|
||||||
|
|||||||
14
.gitignore
vendored
@@ -34,5 +34,17 @@ vite.config.ts.timestamp-*
|
|||||||
# Application specific
|
# Application specific
|
||||||
data
|
data
|
||||||
/frontend/tests/.auth
|
/frontend/tests/.auth
|
||||||
|
/frontend/tests/.report
|
||||||
pocket-id-backend
|
pocket-id-backend
|
||||||
/backend/GeoLite2-City.mmdb
|
/backend/GeoLite2-City.mmdb
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|||||||
302
CHANGELOG.md
@@ -1,3 +1,305 @@
|
|||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.30.0...v) (2025-02-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ability to override the UI configuration with environment variables ([4e85842](https://github.com/pocket-id/pocket-id/commit/4e858420e9d9713e19f3b35c45c882403717f72f))
|
||||||
|
* add warning for only having one passkey configured ([#220](https://github.com/pocket-id/pocket-id/issues/220)) ([39e403d](https://github.com/pocket-id/pocket-id/commit/39e403d00f3870f9e960427653a1d9697da27a6f))
|
||||||
|
* display source in user and group table ([#225](https://github.com/pocket-id/pocket-id/issues/225)) ([9ed2adb](https://github.com/pocket-id/pocket-id/commit/9ed2adb0f8da13725fd9a4ef6a7798c377d13513))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* user linking in ldap group sync ([#222](https://github.com/pocket-id/pocket-id/issues/222)) ([2d78349](https://github.com/pocket-id/pocket-id/commit/2d78349b381d7ca10f47d3c03cef685a576b1b49))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.29.0...v) (2025-02-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add custom ldap search filters ([#216](https://github.com/pocket-id/pocket-id/issues/216)) ([626f87d](https://github.com/pocket-id/pocket-id/commit/626f87d59211f4129098b91dc1d020edb4aca692))
|
||||||
|
* update host configuration to allow external access ([#218](https://github.com/pocket-id/pocket-id/issues/218)) ([bea1158](https://github.com/pocket-id/pocket-id/commit/bea115866fd8e4b15d3281c422d2fb72312758b1))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.28.1...v) (2025-02-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add JSON support in custom claims ([15cde6a](https://github.com/pocket-id/pocket-id/commit/15cde6ac66bc857ac28df545a37c1f4341977595))
|
||||||
|
* add option to disable Caddy in the Docker container ([e864d5d](https://github.com/pocket-id/pocket-id/commit/e864d5dcbff1ef28dc6bf120e4503093a308c5c8))
|
||||||
|
|
||||||
|
## [](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)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* display private IP ranges correctly in audit log ([#139](https://github.com/stonith404/pocket-id/issues/139)) ([72923bb](https://github.com/stonith404/pocket-id/commit/72923bb86dc5d07d56aea98cf03320667944b553))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add save changes dialog before sending test email ([#165](https://github.com/stonith404/pocket-id/issues/165)) ([d02f475](https://github.com/stonith404/pocket-id/commit/d02f4753f3fbda75cd415ebbfe0702765c38c144))
|
||||||
|
* ensure the downloaded GeoLite2 DB is not corrupted & prevent RW race condition ([#138](https://github.com/stonith404/pocket-id/issues/138)) ([f7710f2](https://github.com/stonith404/pocket-id/commit/f7710f298898d322885c1c83680e26faaa0bb800))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.25.1...v) (2025-01-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* support wildcard callback URLs ([8a1db0c](https://github.com/stonith404/pocket-id/commit/8a1db0cb4a5d4b32b4fdc19d41fff688a7c71a56))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* non LDAP users get created with a empty LDAP ID string ([3f02d08](https://github.com/stonith404/pocket-id/commit/3f02d081098ad2caaa60a56eea4705639f80d01f))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.25.0...v) (2025-01-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* disable account details inputs if user is imported from LDAP ([a8b9d60](https://github.com/stonith404/pocket-id/commit/a8b9d60a86e80c10d6fba07072b1d32cec400ecb))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.24.1...v) (2025-01-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add LDAP sync ([#106](https://github.com/stonith404/pocket-id/issues/106)) ([5101b14](https://github.com/stonith404/pocket-id/commit/5101b14eec68a9507e1730994178d0ebe8185876))
|
||||||
|
* allow sign in with email ([#100](https://github.com/stonith404/pocket-id/issues/100)) ([06b90ed](https://github.com/stonith404/pocket-id/commit/06b90eddd645cce57813f2536e4a6a8836548f2b))
|
||||||
|
* automatically authorize client if signed in ([d5dd118](https://github.com/stonith404/pocket-id/commit/d5dd118a3f4ad6eed9ca496c458201bb10f148a0))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* always set secure on cookie ([#130](https://github.com/stonith404/pocket-id/issues/130)) ([fda08ac](https://github.com/stonith404/pocket-id/commit/fda08ac1cd88842e25dc47395ed1288a5cfac4f8))
|
||||||
|
* don't panic if LDAP sync fails on startup ([e284e35](https://github.com/stonith404/pocket-id/commit/e284e352e2b95fac1d098de3d404e8531de4b869))
|
||||||
|
* improve spacing of checkboxes on application configuration page ([090eca2](https://github.com/stonith404/pocket-id/commit/090eca202d198852e6fbf4e6bebaf3b5ada13944))
|
||||||
|
* search input not displayed if response hasn't any items ([05a98eb](https://github.com/stonith404/pocket-id/commit/05a98ebe87d7a88e8b96b144c53250a40d724ec3))
|
||||||
|
* session duration ignored in cookie expiration ([bc8f454](https://github.com/stonith404/pocket-id/commit/bc8f454ea173ecc60e06450a1d22e24207f76714))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.24.0...v) (2025-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* audit log table overflow if row data is long ([4d337a2](https://github.com/stonith404/pocket-id/commit/4d337a20c5cb92ef80bb7402f9b99b08e3ad0b6b))
|
||||||
|
* optional arguments not working with `create-one-time-access-token.sh` ([8885571](https://github.com/stonith404/pocket-id/commit/888557171d61589211b10f70dce405126216ad61))
|
||||||
|
* remove restrictive validation for group names ([be6e25a](https://github.com/stonith404/pocket-id/commit/be6e25a167de8bf07075b46f09d9fc1fa6c74426))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.23.0...v) (2025-01-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add sorting for tables ([fd69830](https://github.com/stonith404/pocket-id/commit/fd69830c2681985e4fd3c5336a2b75c9fb7bc5d4))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* pkce state not correctly reflected in oidc client info ([61d18a9](https://github.com/stonith404/pocket-id/commit/61d18a9d1b167ff59a59523ff00d00ca8f23258d))
|
||||||
|
* send test email to the user that has requested it ([a649c4b](https://github.com/stonith404/pocket-id/commit/a649c4b4a543286123f4d1f3c411fe1a7e2c6d71))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.22.0...v) (2025-01-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add PKCE for non public clients ([adcf3dd](https://github.com/stonith404/pocket-id/commit/adcf3ddc6682794e136a454ef9e69ddd130626a8))
|
||||||
|
* use same table component for OIDC client list as all other lists ([2d31fc2](https://github.com/stonith404/pocket-id/commit/2d31fc2cc9201bb93d296faae622f52c6dcdfebc))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.21.0...v) (2025-01-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add warning if passkeys missing ([2d0bd8d](https://github.com/stonith404/pocket-id/commit/2d0bd8dcbfb73650b7829cb66f40decb284bd73b))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow first and last name of user to be between 1 and 50 characters ([1ff20ca](https://github.com/stonith404/pocket-id/commit/1ff20caa3ccd651f9fb30f958ffb807dfbbcbd8a))
|
||||||
|
* hash in callback url is incorrectly appended ([f6f2736](https://github.com/stonith404/pocket-id/commit/f6f2736bba65eee017f2d8cdaa70621574092869))
|
||||||
|
* make user validation consistent between pages ([333a1a1](https://github.com/stonith404/pocket-id/commit/333a1a18d59f675111f4ed106fa5614ef563c6f4))
|
||||||
|
* passkey can't be added if `PUBLIC_APP_URL` includes a port ([0729ce9](https://github.com/stonith404/pocket-id/commit/0729ce9e1a8dab9912900a01dcd0fbd892718a1a))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.20.1...v) (2024-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* improve error state design for login page ([0716c38](https://github.com/stonith404/pocket-id/commit/0716c38fb8ce7fa719c7fe0df750bdb213786c21))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* OIDC client logo gets removed if other properties get updated ([789d939](https://github.com/stonith404/pocket-id/commit/789d9394a533831e7e2fb8dc3f6b338787336ad8))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.20.0...v) (2024-12-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* `create-one-time-access-token.sh` script not compatible with postgres ([34e3519](https://github.com/stonith404/pocket-id/commit/34e35193f9f3813f6248e60f15080d753e8da7ae))
|
||||||
|
* wrong date time datatype used for read operations with Postgres ([bad901e](https://github.com/stonith404/pocket-id/commit/bad901ea2b661aadd286e5e4bed317e73bd8a70d))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.19.0...v) (2024-12-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for Postgres database provider ([#79](https://github.com/stonith404/pocket-id/issues/79)) ([9d20a98](https://github.com/stonith404/pocket-id/commit/9d20a98dbbc322fa6f0644e8b31e6b97769887ce))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.18.0...v) (2024-11-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **geolite:** add Tailscale IP detection with CGNAT range check ([#77](https://github.com/stonith404/pocket-id/issues/77)) ([edce3d3](https://github.com/stonith404/pocket-id/commit/edce3d337129c9c6e8a60df2122745984ba0f3e0))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.17.0...v) (2024-11-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option to disable TLS for email sending ([f9fa2c6](https://github.com/stonith404/pocket-id/commit/f9fa2c6706a8bf949fe5efd6664dec8c80e18659))
|
||||||
|
* allow empty user and password in SMTP configuration ([a9f4dad](https://github.com/stonith404/pocket-id/commit/a9f4dada321841d3611b15775307228b34e7793f))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* email save toast shows two times ([f2bfc73](https://github.com/stonith404/pocket-id/commit/f2bfc731585ad7424eb8c4c41c18368fc0f75ffc))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.16.0...v) (2024-11-26)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* add option to specify the Max Mind license key for the Geolite2 db
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option to specify the Max Mind license key for the Geolite2 db ([fcf08a4](https://github.com/stonith404/pocket-id/commit/fcf08a4d898160426442bd80830f4431988f4313))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't try to create a new user if the Docker user is not root ([#71](https://github.com/stonith404/pocket-id/issues/71)) ([0e95e9c](https://github.com/stonith404/pocket-id/commit/0e95e9c56f4c3f84982f508fdb6894ba747952b4))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.15.0...v) (2024-11-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add health check ([058084e](https://github.com/stonith404/pocket-id/commit/058084ed64816b12108e25bf04af988fc97772ed))
|
||||||
|
* improve error message for invalid callback url ([f637a89](https://github.com/stonith404/pocket-id/commit/f637a89f579aefb8dc3c3c16a27ef0bc453dfe40))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.14.0...v) (2024-11-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option to skip TLS certificate check and ability to send test email ([653d948](https://github.com/stonith404/pocket-id/commit/653d948f73b61e6d1fd3484398fef1a2a37e6d92))
|
||||||
|
* add PKCE support ([3613ac2](https://github.com/stonith404/pocket-id/commit/3613ac261cf65a2db0620ff16dc6df239f6e5ecd))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mobile layout overflow on application configuration page ([e784093](https://github.com/stonith404/pocket-id/commit/e784093342f9977ea08cac65ff0c3de4d2644872))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.13.1...v) (2024-11-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add audit log event for one time access token sign in ([aca2240](https://github.com/stonith404/pocket-id/commit/aca2240a50a12e849cfb6e1aa56390b000aebae0))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* overflow of pagination control on mobile ([de45398](https://github.com/stonith404/pocket-id/commit/de4539890349153c467013c24c4d6b30feb8fed8))
|
||||||
|
* time displayed incorrectly in audit log ([3d3fb4d](https://github.com/stonith404/pocket-id/commit/3d3fb4d855ef510f2292e98fcaaaf83debb5d3e0))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.13.0...v) (2024-11-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add list empty indicator ([becfc00](https://github.com/stonith404/pocket-id/commit/becfc0004a87c01e18eb92ac85bf4e33f105b6a3))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* errors in middleware do not abort the request ([376d747](https://github.com/stonith404/pocket-id/commit/376d747616b1e835f252d20832c5ae42b8b0b737))
|
||||||
|
* typo in Self-Account Editing description ([5b9f4d7](https://github.com/stonith404/pocket-id/commit/5b9f4d732615f428c13d3317da96a86c5daebd89))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.12.0...v) (2024-10-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ability to define expiration of one time link ([2ccabf8](https://github.com/stonith404/pocket-id/commit/2ccabf835c2c923d6986d9cafb4e878f5110b91a))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.11.0...v) (2024-10-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option to disable self-account editing ([8304065](https://github.com/stonith404/pocket-id/commit/83040656525cf7b6c8f2acf416c5f8f3288f3d48))
|
||||||
|
* add validation to custom claim input ([7bfc3f4](https://github.com/stonith404/pocket-id/commit/7bfc3f43a591287c038187ed5e782de6b9dd738b))
|
||||||
|
* custom claims ([#53](https://github.com/stonith404/pocket-id/issues/53)) ([c056089](https://github.com/stonith404/pocket-id/commit/c056089c6043a825aaaaecf0c57454892a108f1d))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.10.0...v) (2024-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add `email_verified` claim ([5565f60](https://github.com/stonith404/pocket-id/commit/5565f60d6d62ca24bedea337e21effc13e5853a5))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* powered by link text color in light mode ([18c5103](https://github.com/stonith404/pocket-id/commit/18c5103c20ce79abdc0f724cdedd642c09269e78))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.9.0...v) (2024-10-23)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.9.0...v) (2024-10-23)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
19
Dockerfile
@@ -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,8 +20,11 @@ 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
|
||||||
RUN apk add --no-cache caddy
|
# Delete default node user
|
||||||
|
RUN deluser --remove-home node
|
||||||
|
|
||||||
|
RUN apk add --no-cache caddy curl su-exec
|
||||||
COPY ./reverse-proxy /etc/caddy/
|
COPY ./reverse-proxy /etc/caddy/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -30,16 +33,12 @@ COPY --from=frontend-builder /app/frontend/node_modules ./frontend/node_modules
|
|||||||
COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
|
COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
|
||||||
|
|
||||||
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
||||||
COPY --from=backend-builder /app/backend/migrations ./backend/migrations
|
|
||||||
COPY --from=backend-builder /app/backend/GeoLite2-City.mmdb ./backend/GeoLite2-City.mmdb
|
|
||||||
COPY --from=backend-builder /app/backend/email-templates ./backend/email-templates
|
|
||||||
COPY --from=backend-builder /app/backend/images ./backend/images
|
|
||||||
|
|
||||||
COPY ./scripts ./scripts
|
COPY ./scripts ./scripts
|
||||||
RUN chmod +x ./scripts/*.sh
|
RUN chmod +x ./scripts/*.sh
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 80
|
||||||
ENV APP_ENV=production
|
ENV APP_ENV=production
|
||||||
|
|
||||||
# Use a shell form to run both the frontend and backend
|
ENTRYPOINT ["sh", "./scripts/docker/create-user.sh"]
|
||||||
CMD ["sh", "./scripts/docker-entrypoint.sh"]
|
CMD ["sh", "./scripts/docker/entrypoint.sh"]
|
||||||
154
README.md
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
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://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"/>
|
||||||
|
|
||||||
The goal of Pocket ID is to be a simple and easy-to-use. There are other self-hosted OIDC providers like [Keycloak](https://www.keycloak.org/) or [ORY Hydra](https://www.ory.sh/hydra/) but they are often too complex for simple use cases.
|
The goal of Pocket ID is to be a simple and easy-to-use. There are other self-hosted OIDC providers like [Keycloak](https://www.keycloak.org/) or [ORY Hydra](https://www.ory.sh/hydra/) but they are often too complex for simple use cases.
|
||||||
@@ -10,157 +12,9 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
> [!WARNING]
|
Pocket ID can be set up in multiple ways. The easiest and recommended way is to use Docker.
|
||||||
> Pocket ID is in its early stages and may contain bugs.
|
|
||||||
|
|
||||||
### Before you start
|
Visit the [documentation](https://docs.pocket-id.org) for the setup guide and more information.
|
||||||
|
|
||||||
Pocket ID requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), meaning it must be served over HTTPS. This is necessary because Pocket ID uses the [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) which requires a secure context.
|
|
||||||
|
|
||||||
### Installation with Docker (recommended)
|
|
||||||
|
|
||||||
1. Download the `docker-compose.yml` and `.env` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -O https://raw.githubusercontent.com/stonith404/pocket-id/main/docker-compose.yml
|
|
||||||
|
|
||||||
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](#environment-variables) section for more information.
|
|
||||||
3. Run `docker compose up -d`
|
|
||||||
|
|
||||||
You can now sign in with the admin account on `http://localhost/login/setup`.
|
|
||||||
|
|
||||||
### Unraid
|
|
||||||
|
|
||||||
Pocket ID is available as a template on the Community Apps store.
|
|
||||||
|
|
||||||
### Stand-alone Installation
|
|
||||||
|
|
||||||
Required tools:
|
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/download/) >= 20
|
|
||||||
- [Go](https://golang.org/doc/install) >= 1.23
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
|
||||||
- [PM2](https://pm2.keymetrics.io/)
|
|
||||||
- [Caddy](https://caddyserver.com/docs/install) (optional)
|
|
||||||
|
|
||||||
1. Copy the `.env.example` file in the `frontend` and `backend` folder to `.env` and change it so that it fits your needs.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp frontend/.env.example frontend/.env
|
|
||||||
cp backend/.env.example backend/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/stonith404/pocket-id
|
|
||||||
cd pocket-id
|
|
||||||
|
|
||||||
# Checkout the latest version
|
|
||||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
|
||||||
|
|
||||||
# Start the backend
|
|
||||||
cd backend/cmd
|
|
||||||
go build -o ../pocket-id-backend
|
|
||||||
cd ..
|
|
||||||
pm2 start pocket-id-backend --name pocket-id-backend
|
|
||||||
|
|
||||||
# Optional: Download the GeoLite2 city database.
|
|
||||||
# If not downloaded the ip location in the audit log will be empty.
|
|
||||||
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh
|
|
||||||
|
|
||||||
# Start the frontend
|
|
||||||
cd ../frontend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
pm2 start --name pocket-id-frontend --node-args="--env-file .env" build/index.js
|
|
||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
|
||||||
cd ..
|
|
||||||
pm2 start --name pocket-id-caddy caddy -- run --config Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
You can now sign in with the admin account on `http://localhost/login/setup`.
|
|
||||||
|
|
||||||
### Add Pocket ID as an OIDC provider
|
|
||||||
|
|
||||||
You can add a new OIDC client on `https://<your-domain>/settings/admin/oidc-clients`
|
|
||||||
|
|
||||||
After you have added the client, you can obtain the client ID and client secret.
|
|
||||||
|
|
||||||
You may need the following information:
|
|
||||||
|
|
||||||
- **Authorization URL**: `https://<your-domain>/authorize`
|
|
||||||
- **Token URL**: `https://<your-domain>/api/oidc/token`
|
|
||||||
- **Userinfo URL**: `https://<your-domain>/api/oidc/userinfo`
|
|
||||||
- **Certificate URL**: `https://<your-domain>/.well-known/jwks.json`
|
|
||||||
- **OIDC Discovery URL**: `https://<your-domain>/.well-known/openid-configuration`
|
|
||||||
- **Scopes**: At least `openid email`. Optionally you can add `profile` and `groups`.
|
|
||||||
|
|
||||||
### Proxy Services with Pocket ID
|
|
||||||
|
|
||||||
As the goal of Pocket ID is to stay simple, we don't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/) to add authentication to your services that don't support OIDC.
|
|
||||||
|
|
||||||
See the [guide](docs/proxy-services.md) for more information.
|
|
||||||
|
|
||||||
### Update
|
|
||||||
|
|
||||||
#### Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose pull
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Stand-alone
|
|
||||||
|
|
||||||
1. Stop the running services:
|
|
||||||
```bash
|
|
||||||
pm2 delete pocket-id-backend pocket-id-frontend pocket-id-caddy
|
|
||||||
```
|
|
||||||
2. Run the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd pocket-id
|
|
||||||
|
|
||||||
# Checkout the latest version
|
|
||||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
|
||||||
|
|
||||||
# Start the backend
|
|
||||||
cd backend/cmd
|
|
||||||
go build -o ../pocket-id-backend
|
|
||||||
cd ..
|
|
||||||
pm2 start pocket-id-backend --name pocket-id-backend
|
|
||||||
|
|
||||||
# Optional: Update the GeoLite2 city database
|
|
||||||
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh
|
|
||||||
|
|
||||||
# Start the frontend
|
|
||||||
cd ../frontend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
pm2 start build/index.js --name pocket-id-frontend
|
|
||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
|
||||||
cd ..
|
|
||||||
pm2 start caddy --name pocket-id-caddy -- run --config Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment variables
|
|
||||||
|
|
||||||
| Variable | Default Value | Recommended to change | Description |
|
|
||||||
| ---------------------- | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. |
|
|
||||||
| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. |
|
|
||||||
| `DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. |
|
|
||||||
| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. |
|
|
||||||
| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. |
|
|
||||||
| `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. |
|
|
||||||
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
|
||||||
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
PUBLIC_APP_URL=http://localhost
|
PUBLIC_APP_URL=http://localhost
|
||||||
DB_PATH=data/pocket-id.db
|
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
|
||||||
|
DB_PROVIDER=sqlite
|
||||||
|
# MAXMIND_LICENSE_KEY=fixme # needed for IP geolocation in the audit log
|
||||||
|
SQLITE_DB_PATH=data/pocket-id.db
|
||||||
|
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
||||||
UPLOAD_PATH=data/uploads
|
UPLOAD_PATH=data/uploads
|
||||||
PORT=8080
|
PORT=8080
|
||||||
HOST=localhost
|
HOST=0.0.0.0
|
||||||
|
|||||||
3
backend/.gitignore
vendored
@@ -13,4 +13,5 @@
|
|||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
./data
|
./data
|
||||||
|
.env
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/bootstrap"
|
"github.com/pocket-id/pocket-id/backend/internal/bootstrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,50 +1,57 @@
|
|||||||
module github.com/stonith404/pocket-id/backend
|
module github.com/pocket-id/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-contrib/cors v1.7.2
|
|
||||||
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.27.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/sqlite v1.5.6
|
gorm.io/driver/postgres v1.5.11
|
||||||
|
gorm.io/driver/sqlite v1.5.7
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.12.3 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic v1.12.8 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // 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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/jonboulle/clockwork v0.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/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
|
||||||
@@ -54,11 +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.29.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
golang.org/x/text v0.21.0 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
254
backend/go.sum
@@ -1,73 +1,125 @@
|
|||||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
|
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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
|
||||||
|
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
||||||
github.com/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.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
||||||
|
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/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/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 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/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
||||||
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-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
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/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=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
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.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
|
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/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
|
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/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||||
|
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/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||||
|
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/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=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
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=
|
||||||
@@ -79,22 +131,34 @@ 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=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 h1:UihPOz+oIJ5X0JsO7wEkL50fheCODsoZ9r86mJWfNMc=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1/go.mod h1:vPpFrres6g9B5+meBwAd9xnp335KFcLEFW7EqJxBHy0=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2 h1:jG+FaCBv3h6GD5F+oenTfe3+0NmX8sCKjni5k3A5Dek=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2/go.mod h1:rHaQJ5SjfCdL4sqCKa3FhklRcaXga2/qyvmQuA+ZJ6M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 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=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
@@ -105,49 +169,129 @@ 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.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=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.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-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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||||
|
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
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=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
|
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=
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015">
|
|
||||||
<path fill="white" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 434 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015">
|
|
||||||
<path fill="black" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 434 B |
@@ -1,18 +1,21 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
)
|
)
|
||||||
|
|
||||||
// initApplicationImages copies the images from the images directory to the application-images directory
|
// initApplicationImages copies the images from the images directory to the application-images directory
|
||||||
func initApplicationImages() {
|
func initApplicationImages() {
|
||||||
dirPath := common.EnvConfig.UploadPath + "/application-images"
|
dirPath := common.EnvConfig.UploadPath + "/application-images"
|
||||||
|
|
||||||
sourceFiles, err := os.ReadDir("./images")
|
sourceFiles, err := resources.FS.ReadDir("images")
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
log.Fatalf("Error reading directory: %v", err)
|
log.Fatalf("Error reading directory: %v", err)
|
||||||
}
|
}
|
||||||
@@ -27,10 +30,10 @@ func initApplicationImages() {
|
|||||||
if sourceFile.IsDir() || imageAlreadyExists(sourceFile.Name(), destinationFiles) {
|
if sourceFile.IsDir() || imageAlreadyExists(sourceFile.Name(), destinationFiles) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
srcFilePath := "./images/" + sourceFile.Name()
|
srcFilePath := path.Join("images", sourceFile.Name())
|
||||||
destFilePath := dirPath + "/" + sourceFile.Name()
|
destFilePath := path.Join(dirPath, sourceFile.Name())
|
||||||
|
|
||||||
err := utils.CopyFile(srcFilePath, destFilePath)
|
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error copying file: %v", err)
|
log.Fatalf("Error copying file: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/job"
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Bootstrap() {
|
func Bootstrap() {
|
||||||
|
initApplicationImages()
|
||||||
|
|
||||||
db := newDatabase()
|
db := newDatabase()
|
||||||
appConfigService := service.NewAppConfigService(db)
|
appConfigService := service.NewAppConfigService(db)
|
||||||
|
|
||||||
initApplicationImages()
|
|
||||||
job.RegisterJobs(db)
|
|
||||||
initRouter(db, appConfigService)
|
initRouter(db, appConfigService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,22 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"fmt"
|
||||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database"
|
||||||
|
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDatabase() (db *gorm.DB) {
|
func newDatabase() (db *gorm.DB) {
|
||||||
@@ -23,32 +30,63 @@ func newDatabase() (db *gorm.DB) {
|
|||||||
log.Fatalf("failed to get sql.DB: %v", err)
|
log.Fatalf("failed to get sql.DB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
driver, err := sqlite3.WithInstance(sqlDb, &sqlite3.Config{})
|
// Choose the correct driver for the database provider
|
||||||
m, err := migrate.NewWithDatabaseInstance(
|
var driver database.Driver
|
||||||
"file://migrations",
|
switch common.EnvConfig.DbProvider {
|
||||||
"postgres", driver)
|
case common.DbProviderSqlite:
|
||||||
|
driver, err = sqliteMigrate.WithInstance(sqlDb, &sqliteMigrate.Config{})
|
||||||
|
case common.DbProviderPostgres:
|
||||||
|
driver, err = postgresMigrate.WithInstance(sqlDb, &postgresMigrate.Config{})
|
||||||
|
default:
|
||||||
|
log.Fatalf("unsupported database provider: %s", common.EnvConfig.DbProvider)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create migration instance: %v", err)
|
log.Fatalf("failed to create migration driver: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.Up()
|
// Run migrations
|
||||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
if err := migrateDatabase(driver); err != nil {
|
||||||
log.Fatalf("failed to apply migrations: %v", err)
|
log.Fatalf("failed to run migrations: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectDatabase() (db *gorm.DB, err error) {
|
func migrateDatabase(driver database.Driver) error {
|
||||||
dbPath := common.EnvConfig.DBPath
|
// Use the embedded migrations
|
||||||
|
source, err := iofs.New(resources.FS, "migrations/"+string(common.EnvConfig.DbProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create embedded migration source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Use in-memory database for testing
|
m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver)
|
||||||
if common.EnvConfig.AppEnv == "test" {
|
if err != nil {
|
||||||
dbPath = "file::memory:?cache=shared"
|
return fmt.Errorf("failed to create migration instance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Up()
|
||||||
|
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
return fmt.Errorf("failed to apply migrations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectDatabase() (db *gorm.DB, err error) {
|
||||||
|
var dialector gorm.Dialector
|
||||||
|
|
||||||
|
// Choose the correct database provider
|
||||||
|
switch common.EnvConfig.DbProvider {
|
||||||
|
case common.DbProviderSqlite:
|
||||||
|
dialector = sqlite.Open(common.EnvConfig.SqliteDBPath)
|
||||||
|
case common.DbProviderPostgres:
|
||||||
|
dialector = postgres.Open(common.EnvConfig.PostgresConnectionString)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 1; i <= 3; i++ {
|
for i := 1; i <= 3; i++ {
|
||||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
db, err = gorm.Open(dialector, &gorm.Config{
|
||||||
TranslateError: true,
|
TranslateError: true,
|
||||||
Logger: getLogger(),
|
Logger: getLogger(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/controller"
|
"github.com/pocket-id/pocket-id/backend/internal/controller"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/pocket-id/pocket-id/backend/internal/job"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -29,36 +29,46 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) {
|
|||||||
r.Use(gin.Logger())
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
templateDir := os.DirFS(common.EnvConfig.EmailTemplatesPath)
|
emailService, err := service.NewEmailService(appConfigService, db)
|
||||||
emailService, err := service.NewEmailService(appConfigService, templateDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to create email service: %s", err)
|
log.Fatalf("Unable to create email service: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
auditLogService := service.NewAuditLogService(db, appConfigService, emailService)
|
geoLiteService := service.NewGeoLiteService()
|
||||||
|
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)
|
userService := service.NewUserService(db, jwtService, auditLogService, emailService, appConfigService)
|
||||||
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService)
|
customClaimService := service.NewCustomClaimService(db)
|
||||||
|
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)
|
||||||
|
|
||||||
|
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
||||||
|
|
||||||
|
// Setup global middleware
|
||||||
r.Use(middleware.NewCorsMiddleware().Add())
|
r.Use(middleware.NewCorsMiddleware().Add())
|
||||||
r.Use(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60))
|
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||||
|
r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60))
|
||||||
r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false))
|
r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false))
|
||||||
|
|
||||||
// Initialize middleware
|
job.RegisterLdapJobs(ldapService, appConfigService)
|
||||||
|
job.RegisterDbCleanupJobs(db)
|
||||||
|
|
||||||
|
// Initialize middleware for specific routes
|
||||||
jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false)
|
jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false)
|
||||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||||
|
|
||||||
// Set up API routes
|
// Set up API routes
|
||||||
apiGroup := r.Group("/api")
|
apiGroup := r.Group("/api")
|
||||||
controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService)
|
controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService)
|
||||||
controller.NewOidcController(apiGroup, jwtAuthMiddleware, fileSizeLimitMiddleware, oidcService, jwtService)
|
controller.NewOidcController(apiGroup, jwtAuthMiddleware, fileSizeLimitMiddleware, oidcService, jwtService)
|
||||||
controller.NewUserController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), userService)
|
controller.NewUserController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService)
|
||||||
controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService)
|
controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService, emailService, ldapService)
|
||||||
controller.NewAuditLogController(apiGroup, auditLogService, jwtAuthMiddleware)
|
controller.NewAuditLogController(apiGroup, auditLogService, jwtAuthMiddleware)
|
||||||
controller.NewUserGroupController(apiGroup, jwtAuthMiddleware, userGroupService)
|
controller.NewUserGroupController(apiGroup, jwtAuthMiddleware, userGroupService)
|
||||||
|
controller.NewCustomClaimController(apiGroup, jwtAuthMiddleware, customClaimService)
|
||||||
|
|
||||||
// Add test controller in non-production environments
|
// Add test controller in non-production environments
|
||||||
if common.EnvConfig.AppEnv != "production" {
|
if common.EnvConfig.AppEnv != "production" {
|
||||||
|
|||||||
@@ -1,33 +1,61 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"log"
|
)
|
||||||
|
|
||||||
|
type DbProvider string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DbProviderSqlite DbProvider = "sqlite"
|
||||||
|
DbProviderPostgres DbProvider = "postgres"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvConfigSchema struct {
|
type EnvConfigSchema struct {
|
||||||
AppEnv string `env:"APP_ENV"`
|
AppEnv string `env:"APP_ENV"`
|
||||||
AppURL string `env:"PUBLIC_APP_URL"`
|
AppURL string `env:"PUBLIC_APP_URL"`
|
||||||
DBPath string `env:"DB_PATH"`
|
DbProvider DbProvider `env:"DB_PROVIDER"`
|
||||||
UploadPath string `env:"UPLOAD_PATH"`
|
SqliteDBPath string `env:"SQLITE_DB_PATH"`
|
||||||
Port string `env:"BACKEND_PORT"`
|
PostgresConnectionString string `env:"POSTGRES_CONNECTION_STRING"`
|
||||||
Host string `env:"HOST"`
|
UploadPath string `env:"UPLOAD_PATH"`
|
||||||
EmailTemplatesPath string `env:"EMAIL_TEMPLATES_PATH"`
|
Port string `env:"BACKEND_PORT"`
|
||||||
|
Host string `env:"HOST"`
|
||||||
|
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||||
|
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||||
|
UiConfigDisabled bool `env:"PUBLIC_UI_CONFIG_DISABLED"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnvConfig = &EnvConfigSchema{
|
var EnvConfig = &EnvConfigSchema{
|
||||||
AppEnv: "production",
|
AppEnv: "production",
|
||||||
DBPath: "data/pocket-id.db",
|
DbProvider: "sqlite",
|
||||||
UploadPath: "data/uploads",
|
SqliteDBPath: "data/pocket-id.db",
|
||||||
AppURL: "http://localhost",
|
PostgresConnectionString: "",
|
||||||
Port: "8080",
|
UploadPath: "data/uploads",
|
||||||
Host: "localhost",
|
AppURL: "http://localhost",
|
||||||
EmailTemplatesPath: "./email-templates",
|
Port: "8080",
|
||||||
|
Host: "0.0.0.0",
|
||||||
|
MaxMindLicenseKey: "",
|
||||||
|
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
|
||||||
|
UiConfigDisabled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := env.ParseWithOptions(EnvConfig, env.Options{}); err != nil {
|
if err := env.ParseWithOptions(EnvConfig, env.Options{}); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// Validate the environment variables
|
||||||
|
if EnvConfig.DbProvider != DbProviderSqlite && EnvConfig.DbProvider != DbProviderPostgres {
|
||||||
|
log.Fatal("Invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if EnvConfig.DbProvider == DbProviderPostgres && EnvConfig.PostgresConnectionString == "" {
|
||||||
|
log.Fatal("Missing POSTGRES_CONNECTION_STRING environment variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if EnvConfig.DbProvider == DbProviderSqlite && EnvConfig.SqliteDBPath == "" {
|
||||||
|
log.Fatal("Missing SQLITE_DB_PATH environment variable")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,193 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"fmt"
|
||||||
var (
|
"net/http"
|
||||||
ErrUsernameTaken = errors.New("username is already taken")
|
|
||||||
ErrEmailTaken = errors.New("email is already taken")
|
|
||||||
ErrSetupAlreadyCompleted = errors.New("setup already completed")
|
|
||||||
ErrTokenInvalidOrExpired = errors.New("token is invalid or expired")
|
|
||||||
ErrOidcMissingAuthorization = errors.New("missing authorization")
|
|
||||||
ErrOidcGrantTypeNotSupported = errors.New("grant type not supported")
|
|
||||||
ErrOidcMissingClientCredentials = errors.New("client id or secret not provided")
|
|
||||||
ErrOidcClientSecretInvalid = errors.New("invalid client secret")
|
|
||||||
ErrOidcInvalidAuthorizationCode = errors.New("invalid authorization code")
|
|
||||||
ErrOidcInvalidCallbackURL = errors.New("invalid callback URL")
|
|
||||||
ErrFileTypeNotSupported = errors.New("file type not supported")
|
|
||||||
ErrInvalidCredentials = errors.New("no user found with provided credentials")
|
|
||||||
ErrNameAlreadyInUse = errors.New("name is already in use")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AppError interface {
|
||||||
|
error
|
||||||
|
HttpStatusCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error types for various conditions
|
||||||
|
|
||||||
|
type AlreadyInUseError struct {
|
||||||
|
Property string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AlreadyInUseError) Error() string {
|
||||||
|
return fmt.Sprintf("%s is already in use", e.Property)
|
||||||
|
}
|
||||||
|
func (e *AlreadyInUseError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type SetupAlreadyCompletedError struct{}
|
||||||
|
|
||||||
|
func (e *SetupAlreadyCompletedError) Error() string { return "setup already completed" }
|
||||||
|
func (e *SetupAlreadyCompletedError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type TokenInvalidOrExpiredError struct{}
|
||||||
|
|
||||||
|
func (e *TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" }
|
||||||
|
func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type OidcMissingAuthorizationError struct{}
|
||||||
|
|
||||||
|
func (e *OidcMissingAuthorizationError) Error() string { return "missing authorization" }
|
||||||
|
func (e *OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type OidcGrantTypeNotSupportedError struct{}
|
||||||
|
|
||||||
|
func (e *OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" }
|
||||||
|
func (e *OidcGrantTypeNotSupportedError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type OidcMissingClientCredentialsError struct{}
|
||||||
|
|
||||||
|
func (e *OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" }
|
||||||
|
func (e *OidcMissingClientCredentialsError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type OidcClientSecretInvalidError struct{}
|
||||||
|
|
||||||
|
func (e *OidcClientSecretInvalidError) Error() string { return "invalid client secret" }
|
||||||
|
func (e *OidcClientSecretInvalidError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type OidcInvalidAuthorizationCodeError struct{}
|
||||||
|
|
||||||
|
func (e *OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" }
|
||||||
|
func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type OidcInvalidCallbackURLError struct{}
|
||||||
|
|
||||||
|
func (e *OidcInvalidCallbackURLError) Error() string {
|
||||||
|
return "invalid callback URL, it might be necessary for an admin to fix this"
|
||||||
|
}
|
||||||
|
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type FileTypeNotSupportedError struct{}
|
||||||
|
|
||||||
|
func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
||||||
|
func (e *FileTypeNotSupportedError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type InvalidCredentialsError struct{}
|
||||||
|
|
||||||
|
func (e *InvalidCredentialsError) Error() string { return "no user found with provided credentials" }
|
||||||
|
func (e *InvalidCredentialsError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
|
type FileTooLargeError struct {
|
||||||
|
MaxSize string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileTooLargeError) Error() string {
|
||||||
|
return fmt.Sprintf("The file can't be larger than %s", e.MaxSize)
|
||||||
|
}
|
||||||
|
func (e *FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge }
|
||||||
|
|
||||||
|
type NotSignedInError struct{}
|
||||||
|
|
||||||
|
func (e *NotSignedInError) Error() string { return "You are not signed in" }
|
||||||
|
func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||||
|
|
||||||
|
type MissingPermissionError struct{}
|
||||||
|
|
||||||
|
func (e *MissingPermissionError) Error() string {
|
||||||
|
return "You don't have permission to perform this action"
|
||||||
|
}
|
||||||
|
func (e *MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type TooManyRequestsError struct{}
|
||||||
|
|
||||||
|
func (e *TooManyRequestsError) Error() string {
|
||||||
|
return "Too many requests"
|
||||||
|
}
|
||||||
|
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||||
|
|
||||||
|
type ClientIdOrSecretNotProvidedError struct{}
|
||||||
|
|
||||||
|
func (e *ClientIdOrSecretNotProvidedError) Error() string {
|
||||||
|
return "Client id or secret not provided"
|
||||||
|
}
|
||||||
|
func (e *ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type WrongFileTypeError struct {
|
||||||
|
ExpectedFileType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *WrongFileTypeError) Error() string {
|
||||||
|
return fmt.Sprintf("File must be of type %s", e.ExpectedFileType)
|
||||||
|
}
|
||||||
|
func (e *WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type MissingSessionIdError struct{}
|
||||||
|
|
||||||
|
func (e *MissingSessionIdError) Error() string {
|
||||||
|
return "Missing session id"
|
||||||
|
}
|
||||||
|
func (e *MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type ReservedClaimError struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ReservedClaimError) Error() string {
|
||||||
|
return fmt.Sprintf("Claim %s is reserved and can't be used", e.Key)
|
||||||
|
}
|
||||||
|
func (e *ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type DuplicateClaimError struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DuplicateClaimError) Error() string {
|
||||||
|
return fmt.Sprintf("Claim %s is already defined", e.Key)
|
||||||
|
}
|
||||||
|
func (e *DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type AccountEditNotAllowedError struct{}
|
||||||
|
|
||||||
|
func (e *AccountEditNotAllowedError) Error() string {
|
||||||
|
return "You are not allowed to edit your account"
|
||||||
|
}
|
||||||
|
func (e *AccountEditNotAllowedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type OidcInvalidCodeVerifierError struct{}
|
||||||
|
|
||||||
|
func (e *OidcInvalidCodeVerifierError) Error() string {
|
||||||
|
return "Invalid code verifier"
|
||||||
|
}
|
||||||
|
func (e *OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type OidcMissingCodeChallengeError struct{}
|
||||||
|
|
||||||
|
func (e *OidcMissingCodeChallengeError) Error() string {
|
||||||
|
return "Missing code challenge"
|
||||||
|
}
|
||||||
|
func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type LdapUserUpdateError struct{}
|
||||||
|
|
||||||
|
func (e *LdapUserUpdateError) Error() string {
|
||||||
|
return "LDAP users can't be updated"
|
||||||
|
}
|
||||||
|
func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type LdapUserGroupUpdateError struct{}
|
||||||
|
|
||||||
|
func (e *LdapUserGroupUpdateError) Error() string {
|
||||||
|
return "LDAP user groups can't be updated"
|
||||||
|
}
|
||||||
|
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 }
|
||||||
|
|
||||||
|
type UiConfigDisabledError struct{}
|
||||||
|
|
||||||
|
func (e *UiConfigDisabledError) Error() string {
|
||||||
|
return "The configuration can't be changed since the UI configuration is disabled"
|
||||||
|
}
|
||||||
|
func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAppConfigController(
|
func NewAppConfigController(
|
||||||
group *gin.RouterGroup,
|
group *gin.RouterGroup,
|
||||||
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
|
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
|
||||||
appConfigService *service.AppConfigService) {
|
appConfigService *service.AppConfigService,
|
||||||
|
emailService *service.EmailService,
|
||||||
|
ldapService *service.LdapService,
|
||||||
|
) {
|
||||||
|
|
||||||
acc := &AppConfigController{
|
acc := &AppConfigController{
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
|
emailService: emailService,
|
||||||
|
ldapService: ldapService,
|
||||||
}
|
}
|
||||||
group.GET("/application-configuration", acc.listAppConfigHandler)
|
group.GET("/application-configuration", acc.listAppConfigHandler)
|
||||||
group.GET("/application-configuration/all", jwtAuthMiddleware.Add(true), acc.listAllAppConfigHandler)
|
group.GET("/application-configuration/all", jwtAuthMiddleware.Add(true), acc.listAllAppConfigHandler)
|
||||||
@@ -30,22 +35,27 @@ func NewAppConfigController(
|
|||||||
group.PUT("/application-configuration/logo", jwtAuthMiddleware.Add(true), acc.updateLogoHandler)
|
group.PUT("/application-configuration/logo", jwtAuthMiddleware.Add(true), acc.updateLogoHandler)
|
||||||
group.PUT("/application-configuration/favicon", jwtAuthMiddleware.Add(true), acc.updateFaviconHandler)
|
group.PUT("/application-configuration/favicon", jwtAuthMiddleware.Add(true), acc.updateFaviconHandler)
|
||||||
group.PUT("/application-configuration/background-image", jwtAuthMiddleware.Add(true), acc.updateBackgroundImageHandler)
|
group.PUT("/application-configuration/background-image", jwtAuthMiddleware.Add(true), acc.updateBackgroundImageHandler)
|
||||||
|
|
||||||
|
group.POST("/application-configuration/test-email", jwtAuthMiddleware.Add(true), acc.testEmailHandler)
|
||||||
|
group.POST("/application-configuration/sync-ldap", jwtAuthMiddleware.Add(true), acc.syncLdapHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfigController struct {
|
type AppConfigController struct {
|
||||||
appConfigService *service.AppConfigService
|
appConfigService *service.AppConfigService
|
||||||
|
emailService *service.EmailService
|
||||||
|
ldapService *service.LdapService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||||
configuration, err := acc.appConfigService.ListAppConfig(false)
|
configuration, err := acc.appConfigService.ListAppConfig(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var configVariablesDto []dto.PublicAppConfigVariableDto
|
var configVariablesDto []dto.PublicAppConfigVariableDto
|
||||||
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,13 +65,13 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
|||||||
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
||||||
configuration, err := acc.appConfigService.ListAppConfig(true)
|
configuration, err := acc.appConfigService.ListAppConfig(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var configVariablesDto []dto.AppConfigVariableDto
|
var configVariablesDto []dto.AppConfigVariableDto
|
||||||
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,19 +81,19 @@ func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
|||||||
func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
|
func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
|
||||||
var input dto.AppConfigUpdateDto
|
var input dto.AppConfigUpdateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
savedConfigVariables, err := acc.appConfigService.UpdateAppConfig(input)
|
savedConfigVariables, err := acc.appConfigService.UpdateAppConfig(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var configVariablesDto []dto.AppConfigVariableDto
|
var configVariablesDto []dto.AppConfigVariableDto
|
||||||
if err := dto.MapStructList(savedConfigVariables, &configVariablesDto); err != nil {
|
if err := dto.MapStructList(savedConfigVariables, &configVariablesDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,13 +146,13 @@ func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
|
|||||||
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileType := utils.GetFileExtension(file.Filename)
|
fileType := utils.GetFileExtension(file.Filename)
|
||||||
if fileType != "ico" {
|
if fileType != "ico" {
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, "File must be of type .ico")
|
c.Error(&common.WrongFileTypeError{ExpectedFileType: ".ico"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
acc.updateImage(c, "favicon", "ico")
|
acc.updateImage(c, "favicon", "ico")
|
||||||
@@ -164,17 +174,34 @@ func (acc *AppConfigController) getImage(c *gin.Context, name string, imageType
|
|||||||
func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, oldImageType string) {
|
func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, oldImageType string) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = acc.appConfigService.UpdateImage(file, imageName, oldImageType)
|
err = acc.appConfigService.UpdateImage(file, imageName, oldImageType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrFileTypeNotSupported) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
return
|
||||||
} else {
|
}
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
|
||||||
|
err := acc.ldapService.SyncAll()
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
|
||||||
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
|
err := acc.emailService.SendTestEmail(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuditLogController(group *gin.RouterGroup, auditLogService *service.AuditLogService, jwtAuthMiddleware *middleware.JwtAuthMiddleware) {
|
func NewAuditLogController(group *gin.RouterGroup, auditLogService *service.AuditLogService, jwtAuthMiddleware *middleware.JwtAuthMiddleware) {
|
||||||
@@ -24,14 +24,18 @@ type AuditLogController struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||||
|
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||||
|
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
|
|
||||||
// Fetch audit logs for the user
|
// Fetch audit logs for the user
|
||||||
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(userID, page, pageSize)
|
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(userID, sortedPaginationRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
|||||||
var logsDtos []dto.AuditLogDto
|
var logsDtos []dto.AuditLogDto
|
||||||
err = dto.MapStructList(logs, &logsDtos)
|
err = dto.MapStructList(logs, &logsDtos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
backend/internal/controller/custom_claim_controller.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCustomClaimController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, customClaimService *service.CustomClaimService) {
|
||||||
|
wkc := &CustomClaimController{customClaimService: customClaimService}
|
||||||
|
group.GET("/custom-claims/suggestions", jwtAuthMiddleware.Add(true), wkc.getSuggestionsHandler)
|
||||||
|
group.PUT("/custom-claims/user/:userId", jwtAuthMiddleware.Add(true), wkc.UpdateCustomClaimsForUserHandler)
|
||||||
|
group.PUT("/custom-claims/user-group/:userGroupId", jwtAuthMiddleware.Add(true), wkc.UpdateCustomClaimsForUserGroupHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomClaimController struct {
|
||||||
|
customClaimService *service.CustomClaimService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
|
||||||
|
claims, err := ccc.customClaimService.GetSuggestions()
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) {
|
||||||
|
var input []dto.CustomClaimCreateDto
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := c.Param("userId")
|
||||||
|
claims, err := ccc.customClaimService.UpdateCustomClaimsForUser(userId, input)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var customClaimsDto []dto.CustomClaimDto
|
||||||
|
if err := dto.MapStructList(claims, &customClaimsDto); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, customClaimsDto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) {
|
||||||
|
var input []dto.CustomClaimCreateDto
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := c.Param("userGroupId")
|
||||||
|
claims, err := ccc.customClaimService.UpdateCustomClaimsForUserGroup(userId, input)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var customClaimsDto []dto.CustomClaimDto
|
||||||
|
if err := dto.MapStructList(claims, &customClaimsDto); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, customClaimsDto)
|
||||||
|
}
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService, jwtService *service.JwtService) {
|
func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService, jwtService *service.JwtService) {
|
||||||
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)
|
||||||
|
|
||||||
@@ -27,6 +26,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)
|
||||||
@@ -42,19 +42,13 @@ type OidcController struct {
|
|||||||
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||||
var input dto.AuthorizeOidcClientRequestDto
|
var input dto.AuthorizeOidcClientRequestDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
code, callbackURL, err := oc.oidcService.Authorize(input, c.GetString("userID"), c.ClientIP(), c.Request.UserAgent())
|
code, callbackURL, err := oc.oidcService.Authorize(input, c.GetString("userID"), c.ClientIP(), c.Request.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrOidcMissingAuthorization) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusForbidden, err.Error())
|
|
||||||
} else if errors.Is(err, common.ErrOidcInvalidCallbackURL) {
|
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,36 +60,30 @@ 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 {
|
||||||
utils.ControllerError(c, 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 {
|
||||||
if errors.Is(err, common.ErrOidcInvalidCallbackURL) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, 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) {
|
||||||
var input dto.OidcIdTokenDto
|
// Disable cors for this endpoint
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
var input dto.OidcCreateTokensDto
|
||||||
|
|
||||||
if err := c.ShouldBind(&input); err != nil {
|
if err := c.ShouldBind(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,25 +91,13 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
|||||||
clientSecret := input.ClientSecret
|
clientSecret := input.ClientSecret
|
||||||
|
|
||||||
// Client id and secret can also be passed over the Authorization header
|
// Client id and secret can also be passed over the Authorization header
|
||||||
if clientID == "" || clientSecret == "" {
|
if clientID == "" && clientSecret == "" {
|
||||||
var ok bool
|
clientID, clientSecret, _ = c.Request.BasicAuth()
|
||||||
clientID, clientSecret, ok = c.Request.BasicAuth()
|
|
||||||
if !ok {
|
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, "Client id and secret not provided")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, accessToken, err := oc.oidcService.CreateTokens(input.Code, input.GrantType, clientID, clientSecret)
|
idToken, accessToken, err := oc.oidcService.CreateTokens(input.Code, input.GrantType, clientID, clientSecret, input.CodeVerifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrOidcGrantTypeNotSupported) ||
|
c.Error(err)
|
||||||
errors.Is(err, common.ErrOidcMissingClientCredentials) ||
|
|
||||||
errors.Is(err, common.ErrOidcClientSecretInvalid) ||
|
|
||||||
errors.Is(err, common.ErrOidcInvalidAuthorizationCode) {
|
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,14 +108,14 @@ func (oc *OidcController) userInfoHandler(c *gin.Context) {
|
|||||||
token := strings.Split(c.GetHeader("Authorization"), " ")[1]
|
token := strings.Split(c.GetHeader("Authorization"), " ")[1]
|
||||||
jwtClaims, err := oc.jwtService.VerifyOauthAccessToken(token)
|
jwtClaims, err := oc.jwtService.VerifyOauthAccessToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.CustomControllerError(c, http.StatusUnauthorized, common.ErrTokenInvalidOrExpired.Error())
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID := jwtClaims.Subject
|
userID := jwtClaims.Subject
|
||||||
clientId := jwtClaims.Audience[0]
|
clientId := jwtClaims.Audience[0]
|
||||||
claims, err := oc.oidcService.GetUserClaimsForClient(userID, clientId)
|
claims, err := oc.oidcService.GetUserClaimsForClient(userID, clientId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,13 +126,13 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
|||||||
clientId := c.Param("id")
|
clientId := c.Param("id")
|
||||||
client, err := oc.oidcService.GetClient(clientId)
|
client, err := oc.oidcService.GetClient(clientId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -171,23 +147,26 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
|
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||||
|
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
clients, pagination, err := oc.oidcService.ListClients(searchTerm, page, pageSize)
|
clients, pagination, err := oc.oidcService.ListClients(searchTerm, sortedPaginationRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientsDto []dto.OidcClientDto
|
var clientsDto []dto.OidcClientDto
|
||||||
if err := dto.MapStructList(clients, &clientsDto); err != nil {
|
if err := dto.MapStructList(clients, &clientsDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,19 +179,19 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) createClientHandler(c *gin.Context) {
|
func (oc *OidcController) createClientHandler(c *gin.Context) {
|
||||||
var input dto.OidcClientCreateDto
|
var input dto.OidcClientCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := oc.oidcService.CreateClient(input, c.GetString("userID"))
|
client, err := oc.oidcService.CreateClient(input, c.GetString("userID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
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 {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +201,7 @@ func (oc *OidcController) createClientHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
||||||
err := oc.oidcService.DeleteClient(c.Param("id"))
|
err := oc.oidcService.DeleteClient(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,19 +211,19 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
||||||
var input dto.OidcClientCreateDto
|
var input dto.OidcClientCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := oc.oidcService.UpdateClient(c.Param("id"), input)
|
client, err := oc.oidcService.UpdateClient(c.Param("id"), input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
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 {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +233,7 @@ func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
||||||
secret, err := oc.oidcService.CreateClientSecret(c.Param("id"))
|
secret, err := oc.oidcService.CreateClientSecret(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +243,7 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||||
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Param("id"))
|
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,17 +254,13 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = oc.oidcService.UpdateClientLogo(c.Param("id"), file)
|
err = oc.oidcService.UpdateClientLogo(c.Param("id"), file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrFileTypeNotSupported) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,9 +270,31 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
|||||||
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
||||||
err := oc.oidcService.DeleteClientLogo(c.Param("id"))
|
err := oc.oidcService.DeleteClientLogo(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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,10 +1,10 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTestController(group *gin.RouterGroup, testService *service.TestService) {
|
func NewTestController(group *gin.RouterGroup, testService *service.TestService) {
|
||||||
@@ -19,17 +19,22 @@ type TestController struct {
|
|||||||
|
|
||||||
func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
|
func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
|
||||||
if err := tc.TestService.ResetDatabase(); err != nil {
|
if err := tc.TestService.ResetDatabase(); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.TestService.ResetApplicationImages(); err != nil {
|
if err := tc.TestService.ResetApplicationImages(); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.TestService.SeedDatabase(); err != nil {
|
if err := tc.TestService.SeedDatabase(); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tc.TestService.ResetAppConfig(); err != nil {
|
||||||
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"golang.org/x/time/rate"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService) {
|
func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) {
|
||||||
uc := UserController{
|
uc := UserController{
|
||||||
UserService: userService,
|
userService: userService,
|
||||||
|
appConfigService: appConfigService,
|
||||||
}
|
}
|
||||||
|
|
||||||
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
|
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
|
||||||
@@ -30,26 +33,31 @@ func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
|
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
||||||
|
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.requestOneTimeAccessEmailHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserController struct {
|
type UserController struct {
|
||||||
UserService *service.UserService
|
userService *service.UserService
|
||||||
|
appConfigService *service.AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
|
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||||
|
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
users, pagination, err := uc.UserService.ListUsers(searchTerm, page, pageSize)
|
users, pagination, err := uc.userService.ListUsers(searchTerm, sortedPaginationRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersDto []dto.UserDto
|
var usersDto []dto.UserDto
|
||||||
if err := dto.MapStructList(users, &usersDto); err != nil {
|
if err := dto.MapStructList(users, &usersDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,15 +68,15 @@ func (uc *UserController) listUsersHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) getUserHandler(c *gin.Context) {
|
func (uc *UserController) getUserHandler(c *gin.Context) {
|
||||||
user, err := uc.UserService.GetUser(c.Param("id"))
|
user, err := uc.userService.GetUser(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,15 +84,15 @@ func (uc *UserController) getUserHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
||||||
user, err := uc.UserService.GetUser(c.GetString("userID"))
|
user, err := uc.userService.GetUser(c.GetString("userID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +100,8 @@ func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
||||||
if err := uc.UserService.DeleteUser(c.Param("id")); err != nil {
|
if err := uc.userService.DeleteUser(c.Param("id")); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,23 +111,19 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
|||||||
func (uc *UserController) createUserHandler(c *gin.Context) {
|
func (uc *UserController) createUserHandler(c *gin.Context) {
|
||||||
var input dto.UserCreateDto
|
var input dto.UserCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := uc.UserService.CreateUser(input)
|
user, err := uc.userService.CreateUser(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrEmailTaken) || errors.Is(err, common.ErrUsernameTaken) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusConflict, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,71 +135,89 @@ func (uc *UserController) updateUserHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
||||||
|
if uc.appConfigService.DbConfig.AllowOwnAccountEdit.Value != "true" {
|
||||||
|
c.Error(&common.AccountEditNotAllowedError{})
|
||||||
|
return
|
||||||
|
}
|
||||||
uc.updateUser(c, true)
|
uc.updateUser(c, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
||||||
var input dto.OneTimeAccessTokenCreateDto
|
var input dto.OneTimeAccessTokenCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := uc.UserService.CreateOneTimeAccessToken(input.UserID, input.ExpiresAt)
|
token, err := uc.userService.CreateOneTimeAccessToken(input.UserID, input.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{"token": token})
|
c.JSON(http.StatusCreated, gin.H{"token": token})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
|
||||||
user, token, err := uc.UserService.ExchangeOneTimeAccessToken(c.Param("token"))
|
var input dto.OneTimeAccessEmailDto
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := uc.userService.RequestOneTimeAccessEmail(input.Email, input.RedirectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrTokenInvalidOrExpired) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusUnauthorized, err.Error())
|
return
|
||||||
} else {
|
}
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
||||||
|
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Param("token"), c.ClientIP(), c.Request.UserAgent())
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
||||||
user, token, err := uc.UserService.SetupInitialAdmin()
|
user, token, err := uc.userService.SetupInitialAdmin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrSetupAlreadyCompleted) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
||||||
var input dto.UserCreateDto
|
var input dto.UserCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,19 +228,15 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
|||||||
userID = c.Param("id")
|
userID = c.Param("id")
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := uc.UserService.UpdateUser(userID, input, updateOwnUser)
|
user, err := uc.userService.UpdateUser(userID, input, updateOwnUser, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrEmailTaken) || errors.Is(err, common.ErrUsernameTaken) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusConflict, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) {
|
func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) {
|
||||||
@@ -31,26 +28,30 @@ type UserGroupController struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ugc *UserGroupController) list(c *gin.Context) {
|
func (ugc *UserGroupController) list(c *gin.Context) {
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
|
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||||
groups, pagination, err := ugc.UserGroupService.List(searchTerm, page, pageSize)
|
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||||
if err != nil {
|
c.Error(err)
|
||||||
utils.ControllerError(c, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groups, pagination, err := ugc.UserGroupService.List(searchTerm, sortedPaginationRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the user groups to DTOs. The user count can't be mapped directly, so we have to do it manually.
|
||||||
var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups))
|
var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups))
|
||||||
for i, group := range groups {
|
for i, group := range groups {
|
||||||
var groupDto dto.UserGroupDtoWithUserCount
|
var groupDto dto.UserGroupDtoWithUserCount
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(group.ID)
|
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(group.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
groupsDto[i] = groupDto
|
groupsDto[i] = groupDto
|
||||||
@@ -65,13 +66,13 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
|||||||
func (ugc *UserGroupController) get(c *gin.Context) {
|
func (ugc *UserGroupController) get(c *gin.Context) {
|
||||||
group, err := ugc.UserGroupService.Get(c.Param("id"))
|
group, err := ugc.UserGroupService.Get(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDtoWithUsers
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,23 +82,19 @@ func (ugc *UserGroupController) get(c *gin.Context) {
|
|||||||
func (ugc *UserGroupController) create(c *gin.Context) {
|
func (ugc *UserGroupController) create(c *gin.Context) {
|
||||||
var input dto.UserGroupCreateDto
|
var input dto.UserGroupCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := ugc.UserGroupService.Create(input)
|
group, err := ugc.UserGroupService.Create(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrNameAlreadyInUse) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusConflict, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDtoWithUsers
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,23 +104,19 @@ func (ugc *UserGroupController) create(c *gin.Context) {
|
|||||||
func (ugc *UserGroupController) update(c *gin.Context) {
|
func (ugc *UserGroupController) update(c *gin.Context) {
|
||||||
var input dto.UserGroupCreateDto
|
var input dto.UserGroupCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := ugc.UserGroupService.Update(c.Param("id"), input)
|
group, err := ugc.UserGroupService.Update(c.Param("id"), input, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrNameAlreadyInUse) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusConflict, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDtoWithUsers
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +125,7 @@ func (ugc *UserGroupController) update(c *gin.Context) {
|
|||||||
|
|
||||||
func (ugc *UserGroupController) delete(c *gin.Context) {
|
func (ugc *UserGroupController) delete(c *gin.Context) {
|
||||||
if err := ugc.UserGroupService.Delete(c.Param("id")); err != nil {
|
if err := ugc.UserGroupService.Delete(c.Param("id")); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,19 +135,19 @@ func (ugc *UserGroupController) delete(c *gin.Context) {
|
|||||||
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
||||||
var input dto.UserGroupUpdateUsersDto
|
var input dto.UserGroupUpdateUsersDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := ugc.UserGroupService.UpdateUsers(c.Param("id"), input)
|
group, err := ugc.UserGroupService.UpdateUsers(c.Param("id"), input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDtoWithUsers
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, webauthnService *service.WebAuthnService) {
|
func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, webauthnService *service.WebAuthnService, appConfigService *service.AppConfigService) {
|
||||||
wc := &WebauthnController{webAuthnService: webauthnService}
|
wc := &WebauthnController{webAuthnService: webauthnService, appConfigService: appConfigService}
|
||||||
group.GET("/webauthn/register/start", jwtAuthMiddleware.Add(false), wc.beginRegistrationHandler)
|
group.GET("/webauthn/register/start", jwtAuthMiddleware.Add(false), wc.beginRegistrationHandler)
|
||||||
group.POST("/webauthn/register/finish", jwtAuthMiddleware.Add(false), wc.verifyRegistrationHandler)
|
group.POST("/webauthn/register/finish", jwtAuthMiddleware.Add(false), wc.verifyRegistrationHandler)
|
||||||
|
|
||||||
@@ -31,38 +32,39 @@ func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WebauthnController struct {
|
type WebauthnController struct {
|
||||||
webAuthnService *service.WebAuthnService
|
webAuthnService *service.WebAuthnService
|
||||||
|
appConfigService *service.AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
options, err := wc.webAuthnService.BeginRegistration(userID)
|
options, err := wc.webAuthnService.BeginRegistration(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", false, 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 {
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, "Session ID missing")
|
c.Error(&common.MissingSessionIdError{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
credential, err := wc.webAuthnService.VerifyRegistration(sessionID, userID, c.Request)
|
credential, err := wc.webAuthnService.VerifyRegistration(sessionID, userID, c.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var credentialDto dto.WebauthnCredentialDto
|
var credentialDto dto.WebauthnCredentialDto
|
||||||
if err := dto.MapStruct(credential, &credentialDto); err != nil {
|
if err := dto.MapStruct(credential, &credentialDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,46 +74,43 @@ func (wc *WebauthnController) verifyRegistrationHandler(c *gin.Context) {
|
|||||||
func (wc *WebauthnController) beginLoginHandler(c *gin.Context) {
|
func (wc *WebauthnController) beginLoginHandler(c *gin.Context) {
|
||||||
options, err := wc.webAuthnService.BeginLogin()
|
options, err := wc.webAuthnService.BeginLogin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", false, 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 {
|
||||||
utils.CustomControllerError(c, http.StatusBadRequest, "Session ID missing")
|
c.Error(&common.MissingSessionIdError{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body)
|
credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := c.GetString("userID")
|
user, token, err := wc.webAuthnService.VerifyLogin(sessionID, credentialAssertionData, c.ClientIP(), c.Request.UserAgent())
|
||||||
|
|
||||||
user, token, err := wc.webAuthnService.VerifyLogin(sessionID, userID, credentialAssertionData, c.ClientIP(), c.Request.UserAgent())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrInvalidCredentials) {
|
c.Error(err)
|
||||||
utils.CustomControllerError(c, http.StatusUnauthorized, err.Error())
|
|
||||||
} else {
|
|
||||||
utils.ControllerError(c, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDto dto.UserDto
|
var userDto dto.UserDto
|
||||||
if err := dto.MapStruct(user, &userDto); err != nil {
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,13 +118,13 @@ func (wc *WebauthnController) listCredentialsHandler(c *gin.Context) {
|
|||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
credentials, err := wc.webAuthnService.ListCredentials(userID)
|
credentials, err := wc.webAuthnService.ListCredentials(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var credentialDtos []dto.WebauthnCredentialDto
|
var credentialDtos []dto.WebauthnCredentialDto
|
||||||
if err := dto.MapStructList(credentials, &credentialDtos); err != nil {
|
if err := dto.MapStructList(credentials, &credentialDtos); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +137,7 @@ func (wc *WebauthnController) deleteCredentialHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := wc.webAuthnService.DeleteCredential(userID, credentialID)
|
err := wc.webAuthnService.DeleteCredential(userID, credentialID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,19 +150,19 @@ func (wc *WebauthnController) updateCredentialHandler(c *gin.Context) {
|
|||||||
|
|
||||||
var input dto.WebauthnCredentialUpdateDto
|
var input dto.WebauthnCredentialUpdateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
credential, err := wc.webAuthnService.UpdateCredential(userID, credentialID, input.Name)
|
credential, err := wc.webAuthnService.UpdateCredential(userID, credentialID, input.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var credentialDto dto.WebauthnCredentialDto
|
var credentialDto dto.WebauthnCredentialDto
|
||||||
if err := dto.MapStruct(credential, &credentialDto); err != nil {
|
if err := dto.MapStruct(credential, &credentialDto); err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +170,6 @@ func (wc *WebauthnController) updateCredentialHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
||||||
c.SetCookie("access_token", "", 0, "/", "", false, true)
|
cookie.AddAccessTokenCookie(c, 0, "")
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewWellKnownController(group *gin.RouterGroup, jwtService *service.JwtService) {
|
func NewWellKnownController(group *gin.RouterGroup, jwtService *service.JwtService) {
|
||||||
@@ -21,7 +21,7 @@ type WellKnownController struct {
|
|||||||
func (wkc *WellKnownController) jwksHandler(c *gin.Context) {
|
func (wkc *WellKnownController) jwksHandler(c *gin.Context) {
|
||||||
jwk, err := wkc.jwtService.GetJWK()
|
jwk, err := wkc.jwtService.GetJWK()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ControllerError(c, err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
|
|||||||
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
||||||
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
||||||
"scopes_supported": []string{"openid", "profile", "email"},
|
"scopes_supported": []string{"openid", "profile", "email"},
|
||||||
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "preferred_username"},
|
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username"},
|
||||||
"response_types_supported": []string{"code", "id_token"},
|
"response_types_supported": []string{"code", "id_token"},
|
||||||
"subject_types_supported": []string{"public"},
|
"subject_types_supported": []string{"public"},
|
||||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||||
|
|||||||
@@ -12,12 +12,33 @@ type AppConfigVariableDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfigUpdateDto struct {
|
type AppConfigUpdateDto struct {
|
||||||
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
||||||
SessionDuration string `json:"sessionDuration" binding:"required"`
|
SessionDuration string `json:"sessionDuration" binding:"required"`
|
||||||
EmailEnabled string `json:"emailEnabled" binding:"required"`
|
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||||
SmtHost string `json:"smtpHost"`
|
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||||
SmtpPort string `json:"smtpPort"`
|
SmtHost string `json:"smtpHost"`
|
||||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
SmtpPort string `json:"smtpPort"`
|
||||||
SmtpUser string `json:"smtpUser"`
|
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||||
SmtpPassword string `json:"smtpPassword"`
|
SmtpUser string `json:"smtpUser"`
|
||||||
|
SmtpPassword string `json:"smtpPassword"`
|
||||||
|
SmtpTls string `json:"smtpTls"`
|
||||||
|
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||||
|
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
||||||
|
LdapUrl string `json:"ldapUrl"`
|
||||||
|
LdapBindDn string `json:"ldapBindDn"`
|
||||||
|
LdapBindPassword string `json:"ldapBindPassword"`
|
||||||
|
LdapBase string `json:"ldapBase"`
|
||||||
|
LdapUserSearchFilter string `json:"ldapUserSearchFilter"`
|
||||||
|
LdapUserGroupSearchFilter string `json:"ldapUserGroupSearchFilter"`
|
||||||
|
LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
|
||||||
|
LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
|
||||||
|
LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
|
||||||
|
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||||
|
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||||
|
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||||
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
|
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||||
|
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
||||||
|
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
|
||||||
|
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
"time"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuditLogDto struct {
|
type AuditLogDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
|
|
||||||
Event model.AuditLogEvent `json:"event"`
|
Event model.AuditLogEvent `json:"event"`
|
||||||
IpAddress string `json:"ipAddress"`
|
IpAddress string `json:"ipAddress"`
|
||||||
|
|||||||
11
backend/internal/dto/custom_claim_dto.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type CustomClaimDto struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomClaimCreateDto struct {
|
||||||
|
Key string `json:"key" binding:"required,claimKey"`
|
||||||
|
Value string `json:"value" binding:"required"`
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MapStructList maps a list of source structs to a list of destination structs
|
// MapStructList maps a list of source structs to a list of destination structs
|
||||||
|
|||||||
@@ -9,19 +9,32 @@ type PublicOidcClientDto struct {
|
|||||||
type OidcClientDto struct {
|
type OidcClientDto struct {
|
||||||
PublicOidcClientDto
|
PublicOidcClientDto
|
||||||
CallbackURLs []string `json:"callbackURLs"`
|
CallbackURLs []string `json:"callbackURLs"`
|
||||||
CreatedBy UserDto `json:"createdBy"`
|
IsPublic bool `json:"isPublic"`
|
||||||
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
Name string `json:"name" binding:"required,max=50"`
|
Name string `json:"name" binding:"required,max=50"`
|
||||||
CallbackURLs []string `json:"callbackURLs" binding:"required,urlList"`
|
CallbackURLs []string `json:"callbackURLs" binding:"required"`
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeOidcClientRequestDto struct {
|
type AuthorizeOidcClientRequestDto struct {
|
||||||
ClientID string `json:"clientID" binding:"required"`
|
ClientID string `json:"clientID" binding:"required"`
|
||||||
Scope string `json:"scope" binding:"required"`
|
Scope string `json:"scope" binding:"required"`
|
||||||
CallbackURL string `json:"callbackURL"`
|
CallbackURL string `json:"callbackURL"`
|
||||||
Nonce string `json:"nonce"`
|
Nonce string `json:"nonce"`
|
||||||
|
CodeChallenge string `json:"codeChallenge"`
|
||||||
|
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeOidcClientResponseDto struct {
|
type AuthorizeOidcClientResponseDto struct {
|
||||||
@@ -29,9 +42,19 @@ type AuthorizeOidcClientResponseDto struct {
|
|||||||
CallbackURL string `json:"callbackURL"`
|
CallbackURL string `json:"callbackURL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcIdTokenDto struct {
|
type AuthorizationRequiredDto struct {
|
||||||
|
ClientID string `json:"clientID" binding:"required"`
|
||||||
|
Scope string `json:"scope" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
ClientID string `form:"client_id"`
|
ClientID string `form:"client_id"`
|
||||||
ClientSecret string `form:"client_secret"`
|
ClientSecret string `form:"client_secret"`
|
||||||
|
CodeVerifier string `form:"code_verifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcUpdateAllowedUserGroupsDto struct {
|
||||||
|
UserGroupIDs []string `json:"userGroupIds" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,31 @@ package dto
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type UserDto struct {
|
type UserDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email" `
|
Email string `json:"email" `
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=3,max=20"`
|
Username string `json:"username" binding:"required,username,min=2,max=50"`
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" binding:"required,email"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=3,max=30"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50"`
|
||||||
LastName string `json:"lastName" binding:"required,min=3,max=30"`
|
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
|
LdapID string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneTimeAccessTokenCreateDto struct {
|
type OneTimeAccessTokenCreateDto struct {
|
||||||
UserID string `json:"userId" binding:"required"`
|
UserID string `json:"userId" binding:"required"`
|
||||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OneTimeAccessEmailDto struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
RedirectPath string `json:"redirectPath"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
)
|
||||||
|
|
||||||
type UserGroupDtoWithUsers struct {
|
type UserGroupDtoWithUsers struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FriendlyName string `json:"friendlyName"`
|
FriendlyName string `json:"friendlyName"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Users []UserDto `json:"users"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Users []UserDto `json:"users"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserGroupDtoWithUserCount struct {
|
type UserGroupDtoWithUserCount struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FriendlyName string `json:"friendlyName"`
|
FriendlyName string `json:"friendlyName"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UserCount int64 `json:"userCount"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
UserCount int64 `json:"userCount"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserGroupCreateDto struct {
|
type UserGroupCreateDto struct {
|
||||||
FriendlyName string `json:"friendlyName" binding:"required,min=3,max=30"`
|
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50"`
|
||||||
Name string `json:"name" binding:"required,min=3,max=30,userGroupName"`
|
Name string `json:"name" binding:"required,min=2,max=255"`
|
||||||
|
LdapID string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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,21 +4,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validateUrlList validator.Func = func(fl validator.FieldLevel) bool {
|
|
||||||
urls := fl.Field().Interface().([]string)
|
|
||||||
for _, u := range urls {
|
|
||||||
_, err := url.ParseRequestURI(u)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
||||||
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
||||||
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
||||||
@@ -28,27 +16,21 @@ var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
|||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
var validateUserGroupName validator.Func = func(fl validator.FieldLevel) bool {
|
var validateClaimKey validator.Func = func(fl validator.FieldLevel) bool {
|
||||||
// [a-z0-9_] : The group name can only contain lowercase letters, numbers, and underscores
|
// The string can only contain letters and numbers
|
||||||
regex := "^[a-z0-9_]+$"
|
regex := "^[A-Za-z0-9]*$"
|
||||||
matched, _ := regexp.MatchString(regex, fl.Field().String())
|
matched, _ := regexp.MatchString(regex, fl.Field().String())
|
||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
|
||||||
if err := v.RegisterValidation("urlList", validateUrlList); err != nil {
|
|
||||||
log.Fatalf("Failed to register custom validation: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
if err := v.RegisterValidation("username", validateUsername); err != nil {
|
if err := v.RegisterValidation("username", validateUsername); err != nil {
|
||||||
log.Fatalf("Failed to register custom validation: %v", err)
|
log.Fatalf("Failed to register custom validation: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
if err := v.RegisterValidation("userGroupName", validateUserGroupName); err != nil {
|
if err := v.RegisterValidation("claimKey", validateClaimKey); err != nil {
|
||||||
log.Fatalf("Failed to register custom validation: %v", err)
|
log.Fatalf("Failed to register custom validation: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"time"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebauthnCredentialDto struct {
|
type WebauthnCredentialDto struct {
|
||||||
@@ -15,7 +15,7 @@ type WebauthnCredentialDto struct {
|
|||||||
BackupEligible bool `json:"backupEligible"`
|
BackupEligible bool `json:"backupEligible"`
|
||||||
BackupState bool `json:"backupState"`
|
BackupState bool `json:"backupState"`
|
||||||
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebauthnCredentialUpdateDto struct {
|
type WebauthnCredentialUpdateDto struct {
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-co-op/gocron/v2"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterJobs(db *gorm.DB) {
|
func RegisterDbCleanupJobs(db *gorm.DB) {
|
||||||
scheduler, err := gocron.NewScheduler()
|
scheduler, err := gocron.NewScheduler()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create a new scheduler: %s", err)
|
log.Fatalf("Failed to create a new scheduler: %s", err)
|
||||||
@@ -29,22 +31,22 @@ type Jobs struct {
|
|||||||
|
|
||||||
// ClearWebauthnSessions deletes WebAuthn sessions that have expired
|
// ClearWebauthnSessions deletes WebAuthn sessions that have expired
|
||||||
func (j *Jobs) clearWebauthnSessions() error {
|
func (j *Jobs) clearWebauthnSessions() error {
|
||||||
return j.db.Delete(&model.WebauthnSession{}, "expires_at < ?", time.Now().Unix()).Error
|
return j.db.Delete(&model.WebauthnSession{}, "expires_at < ?", datatype.DateTime(time.Now())).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearOneTimeAccessTokens deletes one-time access tokens that have expired
|
// ClearOneTimeAccessTokens deletes one-time access tokens that have expired
|
||||||
func (j *Jobs) clearOneTimeAccessTokens() error {
|
func (j *Jobs) clearOneTimeAccessTokens() error {
|
||||||
return j.db.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", time.Now().Unix()).Error
|
return j.db.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", datatype.DateTime(time.Now())).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearOidcAuthorizationCodes deletes OIDC authorization codes that have expired
|
// ClearOidcAuthorizationCodes deletes OIDC authorization codes that have expired
|
||||||
func (j *Jobs) clearOidcAuthorizationCodes() error {
|
func (j *Jobs) clearOidcAuthorizationCodes() error {
|
||||||
return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", time.Now().Unix()).Error
|
return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", datatype.DateTime(time.Now())).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearAuditLogs deletes audit logs older than 90 days
|
// ClearAuditLogs deletes audit logs older than 90 days
|
||||||
func (j *Jobs) clearAuditLogs() error {
|
func (j *Jobs) clearAuditLogs() error {
|
||||||
return j.db.Delete(&model.AuditLog{}, "created_at < ?", time.Now().AddDate(0, 0, -90).Unix()).Error
|
return j.db.Delete(&model.AuditLog{}, "created_at < ?", datatype.DateTime(time.Now().AddDate(0, 0, -90))).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerJob(scheduler gocron.Scheduler, name string, interval string, job func() error) {
|
func registerJob(scheduler gocron.Scheduler, name string, interval string, job func() error) {
|
||||||
|
|||||||
39
backend/internal/job/ldap_job.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LdapJobs struct {
|
||||||
|
ldapService *service.LdapService
|
||||||
|
appConfigService *service.AppConfigService
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterLdapJobs(ldapService *service.LdapService, appConfigService *service.AppConfigService) {
|
||||||
|
jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService}
|
||||||
|
|
||||||
|
scheduler, err := gocron.NewScheduler()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create a new scheduler: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the job to run every hour
|
||||||
|
registerJob(scheduler, "SyncLdap", "0 * * * *", jobs.syncLdap)
|
||||||
|
|
||||||
|
// Run the job immediately on startup
|
||||||
|
if err := jobs.syncLdap(); err != nil {
|
||||||
|
log.Printf("Failed to sync LDAP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *LdapJobs) syncLdap() error {
|
||||||
|
if j.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||||
|
return j.ldapService.SyncAll()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CorsMiddleware struct{}
|
type CorsMiddleware struct{}
|
||||||
@@ -15,10 +12,22 @@ func NewCorsMiddleware() *CorsMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *CorsMiddleware) Add() gin.HandlerFunc {
|
func (m *CorsMiddleware) Add() gin.HandlerFunc {
|
||||||
return cors.New(cors.Config{
|
return func(c *gin.Context) {
|
||||||
AllowOrigins: []string{common.EnvConfig.AppURL},
|
// Allow all origins for the token endpoint
|
||||||
AllowMethods: []string{"*"},
|
if c.FullPath() == "/api/oidc/token" {
|
||||||
AllowHeaders: []string{"*"},
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
MaxAge: 12 * time.Hour,
|
} else {
|
||||||
})
|
c.Writer.Header().Set("Access-Control-Allow-Origin", common.EnvConfig.AppURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
|
||||||
|
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,68 @@
|
|||||||
package utils
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
"fmt"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
import (
|
type ErrorHandlerMiddleware struct{}
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ControllerError(c *gin.Context, err error) {
|
func NewErrorHandlerMiddleware() *ErrorHandlerMiddleware {
|
||||||
// Check for record not found errors
|
return &ErrorHandlerMiddleware{}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
}
|
||||||
CustomControllerError(c, http.StatusNotFound, "Record not found")
|
|
||||||
return
|
func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Next()
|
||||||
|
for _, err := range c.Errors {
|
||||||
|
|
||||||
|
// Check for record not found errors
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
errorResponse(c, http.StatusNotFound, "Record not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for validation errors
|
||||||
|
var validationErrors validator.ValidationErrors
|
||||||
|
if errors.As(err, &validationErrors) {
|
||||||
|
message := handleValidationError(validationErrors)
|
||||||
|
errorResponse(c, http.StatusBadRequest, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for slice validation errors
|
||||||
|
var sliceValidationErrors binding.SliceValidationError
|
||||||
|
if errors.As(err, &sliceValidationErrors) {
|
||||||
|
if errors.As(sliceValidationErrors[0], &validationErrors) {
|
||||||
|
message := handleValidationError(validationErrors)
|
||||||
|
errorResponse(c, http.StatusBadRequest, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var appErr common.AppError
|
||||||
|
if errors.As(err, &appErr) {
|
||||||
|
errorResponse(c, appErr.HttpStatusCode(), appErr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for validation errors
|
func errorResponse(c *gin.Context, statusCode int, message string) {
|
||||||
var validationErrors validator.ValidationErrors
|
// Capitalize the first letter of the message
|
||||||
if errors.As(err, &validationErrors) {
|
message = strings.ToUpper(message[:1]) + message[1:]
|
||||||
message := handleValidationError(validationErrors)
|
c.JSON(statusCode, gin.H{"error": message})
|
||||||
CustomControllerError(c, http.StatusBadRequest, message)
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleValidationError(validationErrors validator.ValidationErrors) string {
|
func handleValidationError(validationErrors validator.ValidationErrors) string {
|
||||||
@@ -53,8 +84,6 @@ func handleValidationError(validationErrors validator.ValidationErrors) string {
|
|||||||
errorMessage = fmt.Sprintf("%s must be at least %s characters long", fieldName, ve.Param())
|
errorMessage = fmt.Sprintf("%s must be at least %s characters long", fieldName, ve.Param())
|
||||||
case "max":
|
case "max":
|
||||||
errorMessage = fmt.Sprintf("%s must be at most %s characters long", fieldName, ve.Param())
|
errorMessage = fmt.Sprintf("%s must be at most %s characters long", fieldName, ve.Param())
|
||||||
case "urlList":
|
|
||||||
errorMessage = fmt.Sprintf("%s must be a list of valid URLs", fieldName)
|
|
||||||
default:
|
default:
|
||||||
errorMessage = fmt.Sprintf("%s is invalid", fieldName)
|
errorMessage = fmt.Sprintf("%s is invalid", fieldName)
|
||||||
}
|
}
|
||||||
@@ -67,9 +96,3 @@ func handleValidationError(validationErrors validator.ValidationErrors) string {
|
|||||||
|
|
||||||
return combinedErrors
|
return combinedErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
func CustomControllerError(c *gin.Context, statusCode int, message string) {
|
|
||||||
// Capitalize the first letter of the message
|
|
||||||
message = strings.ToUpper(message[:1]) + message[1:]
|
|
||||||
c.JSON(statusCode, gin.H{"error": message})
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,10 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileSizeLimitMiddleware struct{}
|
type FileSizeLimitMiddleware struct{}
|
||||||
@@ -17,7 +18,8 @@ func (m *FileSizeLimitMiddleware) Add(maxSize int64) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
|
||||||
if err := c.Request.ParseMultipartForm(maxSize); err != nil {
|
if err := c.Request.ParseMultipartForm(maxSize); err != nil {
|
||||||
utils.CustomControllerError(c, http.StatusRequestEntityTooLarge, fmt.Sprintf("The file can't be larger than %s bytes", formatFileSize(maxSize)))
|
err = &common.FileTooLargeError{MaxSize: formatFileSize(maxSize)}
|
||||||
|
c.Error(err)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JwtAuthMiddleware struct {
|
type JwtAuthMiddleware struct {
|
||||||
@@ -20,7 +21,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 {
|
||||||
@@ -29,7 +30,7 @@ func (m *JwtAuthMiddleware) Add(adminOnly bool) gin.HandlerFunc {
|
|||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
utils.CustomControllerError(c, http.StatusUnauthorized, "You're not signed in")
|
c.Error(&common.NotSignedInError{})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -40,14 +41,14 @@ func (m *JwtAuthMiddleware) Add(adminOnly bool) gin.HandlerFunc {
|
|||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
utils.CustomControllerError(c, http.StatusUnauthorized, "You're not signed in")
|
c.Error(&common.NotSignedInError{})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is an admin
|
// Check if the user is an admin
|
||||||
if adminOnly && !claims.IsAdmin {
|
if adminOnly && !claims.IsAdmin {
|
||||||
utils.CustomControllerError(c, http.StatusForbidden, "You don't have permission to access this resource")
|
c.Error(&common.MissingPermissionError{})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
@@ -18,8 +17,12 @@ func NewRateLimitMiddleware() *RateLimitMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RateLimitMiddleware) Add(limit rate.Limit, burst int) gin.HandlerFunc {
|
func (m *RateLimitMiddleware) Add(limit rate.Limit, burst int) gin.HandlerFunc {
|
||||||
|
// Map to store the rate limiters per IP
|
||||||
|
var clients = make(map[string]*client)
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
// Start the cleanup routine
|
// Start the cleanup routine
|
||||||
go cleanupClients()
|
go cleanupClients(&mu, clients)
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
ip := c.ClientIP()
|
ip := c.ClientIP()
|
||||||
@@ -31,9 +34,9 @@ func (m *RateLimitMiddleware) Add(limit rate.Limit, burst int) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limiter := getLimiter(ip, limit, burst)
|
limiter := getLimiter(ip, limit, burst, &mu, clients)
|
||||||
if !limiter.Allow() {
|
if !limiter.Allow() {
|
||||||
utils.CustomControllerError(c, http.StatusTooManyRequests, "Too many requests. Please wait a while before trying again.")
|
c.Error(&common.TooManyRequestsError{})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,12 +50,8 @@ type client struct {
|
|||||||
lastSeen time.Time
|
lastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map to store the rate limiters per IP
|
|
||||||
var clients = make(map[string]*client)
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
// Cleanup routine to remove stale clients that haven't been seen for a while
|
// Cleanup routine to remove stale clients that haven't been seen for a while
|
||||||
func cleanupClients() {
|
func cleanupClients(mu *sync.Mutex, clients map[string]*client) {
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Minute)
|
time.Sleep(time.Minute)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -66,7 +65,7 @@ func cleanupClients() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getLimiter retrieves the rate limiter for a given IP address, creating one if it doesn't exist
|
// getLimiter retrieves the rate limiter for a given IP address, creating one if it doesn't exist
|
||||||
func getLimiter(ip string, limit rate.Limit, burst int) *rate.Limiter {
|
func getLimiter(ip string, limit rate.Limit, burst int, mu *sync.Mutex, clients map[string]*client) *rate.Limiter {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,49 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type AppConfigVariable struct {
|
type AppConfigVariable struct {
|
||||||
Key string `gorm:"primaryKey;not null"`
|
Key string `gorm:"primaryKey;not null"`
|
||||||
Type string
|
Type string
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
IsInternal bool
|
IsInternal bool
|
||||||
Value string
|
Value string
|
||||||
|
DefaultValue string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
|
// General
|
||||||
AppName AppConfigVariable
|
AppName AppConfigVariable
|
||||||
|
SessionDuration AppConfigVariable
|
||||||
|
EmailsVerified AppConfigVariable
|
||||||
|
AllowOwnAccountEdit AppConfigVariable
|
||||||
|
// Internal
|
||||||
BackgroundImageType AppConfigVariable
|
BackgroundImageType AppConfigVariable
|
||||||
LogoLightImageType AppConfigVariable
|
LogoLightImageType AppConfigVariable
|
||||||
LogoDarkImageType AppConfigVariable
|
LogoDarkImageType AppConfigVariable
|
||||||
SessionDuration AppConfigVariable
|
// Email
|
||||||
|
SmtpHost AppConfigVariable
|
||||||
EmailEnabled AppConfigVariable
|
SmtpPort AppConfigVariable
|
||||||
SmtpHost AppConfigVariable
|
SmtpFrom AppConfigVariable
|
||||||
SmtpPort AppConfigVariable
|
SmtpUser AppConfigVariable
|
||||||
SmtpFrom AppConfigVariable
|
SmtpPassword AppConfigVariable
|
||||||
SmtpUser AppConfigVariable
|
SmtpTls AppConfigVariable
|
||||||
SmtpPassword AppConfigVariable
|
SmtpSkipCertVerify AppConfigVariable
|
||||||
|
EmailLoginNotificationEnabled AppConfigVariable
|
||||||
|
EmailOneTimeAccessEnabled AppConfigVariable
|
||||||
|
// LDAP
|
||||||
|
LdapEnabled AppConfigVariable
|
||||||
|
LdapUrl AppConfigVariable
|
||||||
|
LdapBindDn AppConfigVariable
|
||||||
|
LdapBindPassword AppConfigVariable
|
||||||
|
LdapBase AppConfigVariable
|
||||||
|
LdapUserSearchFilter AppConfigVariable
|
||||||
|
LdapUserGroupSearchFilter AppConfigVariable
|
||||||
|
LdapSkipCertVerify AppConfigVariable
|
||||||
|
LdapAttributeUserUniqueIdentifier AppConfigVariable
|
||||||
|
LdapAttributeUserUsername AppConfigVariable
|
||||||
|
LdapAttributeUserEmail AppConfigVariable
|
||||||
|
LdapAttributeUserFirstName AppConfigVariable
|
||||||
|
LdapAttributeUserLastName AppConfigVariable
|
||||||
|
LdapAttributeGroupUniqueIdentifier AppConfigVariable
|
||||||
|
LdapAttributeGroupName AppConfigVariable
|
||||||
|
LdapAttributeAdminGroup AppConfigVariable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
type AuditLog struct {
|
type AuditLog struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Event AuditLogEvent
|
Event AuditLogEvent `sortable:"true"`
|
||||||
IpAddress string
|
IpAddress string `sortable:"true"`
|
||||||
Country string
|
Country string `sortable:"true"`
|
||||||
City string
|
City string `sortable:"true"`
|
||||||
UserAgent string
|
UserAgent string `sortable:"true"`
|
||||||
UserID string
|
UserID string
|
||||||
Data AuditLogData
|
Data AuditLogData
|
||||||
}
|
}
|
||||||
@@ -23,9 +23,10 @@ type AuditLogData map[string]string
|
|||||||
type AuditLogEvent string
|
type AuditLogEvent string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AuditLogEventSignIn AuditLogEvent = "SIGN_IN"
|
AuditLogEventSignIn AuditLogEvent = "SIGN_IN"
|
||||||
AuditLogEventClientAuthorization AuditLogEvent = "CLIENT_AUTHORIZATION"
|
AuditLogEventOneTimeAccessTokenSignIn AuditLogEvent = "TOKEN_SIGN_IN"
|
||||||
AuditLogEventNewClientAuthorization AuditLogEvent = "NEW_CLIENT_AUTHORIZATION"
|
AuditLogEventClientAuthorization AuditLogEvent = "CLIENT_AUTHORIZATION"
|
||||||
|
AuditLogEventNewClientAuthorization AuditLogEvent = "NEW_CLIENT_AUTHORIZATION"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scan and Value methods for GORM to handle the custom type
|
// Scan and Value methods for GORM to handle the custom type
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
|
||||||
model "github.com/stonith404/pocket-id/backend/internal/model/types"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
model "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Base contains common columns for all tables.
|
// Base contains common columns for all tables.
|
||||||
type Base struct {
|
type Base struct {
|
||||||
ID string `gorm:"primaryKey;not null"`
|
ID string `gorm:"primaryKey;not null"`
|
||||||
CreatedAt model.DateTime
|
CreatedAt model.DateTime `sortable:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) BeforeCreate(_ *gorm.DB) (err error) {
|
func (b *Base) BeforeCreate(_ *gorm.DB) (err error) {
|
||||||
|
|||||||
11
backend/internal/model/custom_claim.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type CustomClaim struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
|
||||||
|
UserID *string
|
||||||
|
UserGroupID *string
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import (
|
|||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
datatype "github.com/stonith404/pocket-id/backend/internal/model/types"
|
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,10 +21,12 @@ type UserAuthorizedOidcClient struct {
|
|||||||
type OidcAuthorizationCode struct {
|
type OidcAuthorizationCode struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Code string
|
Code string
|
||||||
Scope string
|
Scope string
|
||||||
Nonce string
|
Nonce string
|
||||||
ExpiresAt datatype.DateTime
|
CodeChallenge *string
|
||||||
|
CodeChallengeMethodSha256 *bool
|
||||||
|
ExpiresAt datatype.DateTime
|
||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
User User
|
User User
|
||||||
@@ -34,14 +37,17 @@ type OidcAuthorizationCode struct {
|
|||||||
type OidcClient struct {
|
type OidcClient struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Name string
|
Name string `sortable:"true"`
|
||||||
Secret string
|
Secret string
|
||||||
CallbackURLs CallbackURLs
|
CallbackURLs CallbackURLs
|
||||||
ImageType *string
|
ImageType *string
|
||||||
HasLogo bool `gorm:"-"`
|
HasLogo bool `gorm:"-"`
|
||||||
|
IsPublic 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) {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package datatype
|
|||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DateTime custom type for time.Time to store date as unix timestamp in the database
|
// DateTime custom type for time.Time to store date as unix timestamp for sqlite and as date for postgres
|
||||||
type DateTime time.Time
|
type DateTime time.Time
|
||||||
|
|
||||||
func (date *DateTime) Scan(value interface{}) (err error) {
|
func (date *DateTime) Scan(value interface{}) (err error) {
|
||||||
@@ -14,7 +16,11 @@ func (date *DateTime) Scan(value interface{}) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (date DateTime) Value() (driver.Value, error) {
|
func (date DateTime) Value() (driver.Value, error) {
|
||||||
return time.Time(date).Unix(), nil
|
if common.EnvConfig.DbProvider == common.DbProviderSqlite {
|
||||||
|
return time.Time(date).Unix(), nil
|
||||||
|
} else {
|
||||||
|
return time.Time(date), nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (date DateTime) UTC() time.Time {
|
func (date DateTime) UTC() time.Time {
|
||||||
|
|||||||
@@ -3,20 +3,22 @@ package model
|
|||||||
import (
|
import (
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Username string
|
Username string `sortable:"true"`
|
||||||
Email string
|
Email string `sortable:"true"`
|
||||||
FirstName string
|
FirstName string `sortable:"true"`
|
||||||
LastName string
|
LastName string `sortable:"true"`
|
||||||
IsAdmin bool
|
IsAdmin bool `sortable:"true"`
|
||||||
|
LdapID *string
|
||||||
|
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
CustomClaims []CustomClaim
|
||||||
Credentials []WebauthnCredential
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
|
Credentials []WebauthnCredential
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) WebAuthnID() []byte { return []byte(u.ID) }
|
func (u User) WebAuthnID() []byte { return []byte(u.ID) }
|
||||||
@@ -32,7 +34,7 @@ func (u User) WebAuthnCredentials() []webauthn.Credential {
|
|||||||
|
|
||||||
for i, credential := range u.Credentials {
|
for i, credential := range u.Credentials {
|
||||||
credentials[i] = webauthn.Credential{
|
credentials[i] = webauthn.Credential{
|
||||||
ID: []byte(credential.CredentialID),
|
ID: credential.CredentialID,
|
||||||
AttestationType: credential.AttestationType,
|
AttestationType: credential.AttestationType,
|
||||||
PublicKey: credential.PublicKey,
|
PublicKey: credential.PublicKey,
|
||||||
Transport: credential.Transport,
|
Transport: credential.Transport,
|
||||||
@@ -58,6 +60,8 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
|
|||||||
return descriptors
|
return descriptors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u User) FullName() string { return u.FirstName + " " + u.LastName }
|
||||||
|
|
||||||
type OneTimeAccessToken struct {
|
type OneTimeAccessToken struct {
|
||||||
Base
|
Base
|
||||||
Token string
|
Token string
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package model
|
|||||||
|
|
||||||
type UserGroup struct {
|
type UserGroup struct {
|
||||||
Base
|
Base
|
||||||
FriendlyName string
|
FriendlyName string `sortable:"true"`
|
||||||
Name string `gorm:"unique"`
|
Name string `sortable:"true"`
|
||||||
|
LdapID *string
|
||||||
Users []User `gorm:"many2many:user_groups_users;"`
|
Users []User `gorm:"many2many:user_groups_users;"`
|
||||||
|
CustomClaims []CustomClaim
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import (
|
|||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebauthnSession struct {
|
type WebauthnSession struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Challenge string
|
Challenge string
|
||||||
ExpiresAt time.Time
|
ExpiresAt datatype.DateTime
|
||||||
UserVerification string
|
UserVerification string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ type WebauthnCredential struct {
|
|||||||
Base
|
Base
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
CredentialID string
|
CredentialID []byte
|
||||||
PublicKey []byte
|
PublicKey []byte
|
||||||
AttestationType string
|
AttestationType string
|
||||||
Transport AuthenticatorTransportList
|
Transport AuthenticatorTransportList
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppConfigService struct {
|
type AppConfigService struct {
|
||||||
@@ -30,40 +31,49 @@ func NewAppConfigService(db *gorm.DB) *AppConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var defaultDbConfig = model.AppConfig{
|
var defaultDbConfig = model.AppConfig{
|
||||||
|
// General
|
||||||
AppName: model.AppConfigVariable{
|
AppName: model.AppConfigVariable{
|
||||||
Key: "appName",
|
Key: "appName",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
IsPublic: true,
|
IsPublic: true,
|
||||||
Value: "Pocket ID",
|
DefaultValue: "Pocket ID",
|
||||||
},
|
},
|
||||||
SessionDuration: model.AppConfigVariable{
|
SessionDuration: model.AppConfigVariable{
|
||||||
Key: "sessionDuration",
|
Key: "sessionDuration",
|
||||||
Type: "number",
|
Type: "number",
|
||||||
Value: "60",
|
DefaultValue: "60",
|
||||||
},
|
},
|
||||||
|
EmailsVerified: model.AppConfigVariable{
|
||||||
|
Key: "emailsVerified",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
AllowOwnAccountEdit: model.AppConfigVariable{
|
||||||
|
Key: "allowOwnAccountEdit",
|
||||||
|
Type: "bool",
|
||||||
|
IsPublic: true,
|
||||||
|
DefaultValue: "true",
|
||||||
|
},
|
||||||
|
// Internal
|
||||||
BackgroundImageType: model.AppConfigVariable{
|
BackgroundImageType: model.AppConfigVariable{
|
||||||
Key: "backgroundImageType",
|
Key: "backgroundImageType",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
Value: "jpg",
|
DefaultValue: "jpg",
|
||||||
},
|
},
|
||||||
LogoLightImageType: model.AppConfigVariable{
|
LogoLightImageType: model.AppConfigVariable{
|
||||||
Key: "logoLightImageType",
|
Key: "logoLightImageType",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
Value: "svg",
|
DefaultValue: "svg",
|
||||||
},
|
},
|
||||||
LogoDarkImageType: model.AppConfigVariable{
|
LogoDarkImageType: model.AppConfigVariable{
|
||||||
Key: "logoDarkImageType",
|
Key: "logoDarkImageType",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
Value: "svg",
|
DefaultValue: "svg",
|
||||||
},
|
|
||||||
EmailEnabled: model.AppConfigVariable{
|
|
||||||
Key: "emailEnabled",
|
|
||||||
Type: "bool",
|
|
||||||
Value: "false",
|
|
||||||
},
|
},
|
||||||
|
// Email
|
||||||
SmtpHost: model.AppConfigVariable{
|
SmtpHost: model.AppConfigVariable{
|
||||||
Key: "smtpHost",
|
Key: "smtpHost",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -84,20 +94,121 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
Key: "smtpPassword",
|
Key: "smtpPassword",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
|
SmtpTls: model.AppConfigVariable{
|
||||||
|
Key: "smtpTls",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "true",
|
||||||
|
},
|
||||||
|
SmtpSkipCertVerify: model.AppConfigVariable{
|
||||||
|
Key: "smtpSkipCertVerify",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
EmailLoginNotificationEnabled: model.AppConfigVariable{
|
||||||
|
Key: "emailLoginNotificationEnabled",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
EmailOneTimeAccessEnabled: model.AppConfigVariable{
|
||||||
|
Key: "emailOneTimeAccessEnabled",
|
||||||
|
Type: "bool",
|
||||||
|
IsPublic: true,
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
// LDAP
|
||||||
|
LdapEnabled: model.AppConfigVariable{
|
||||||
|
Key: "ldapEnabled",
|
||||||
|
Type: "bool",
|
||||||
|
IsPublic: true,
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
LdapUrl: model.AppConfigVariable{
|
||||||
|
Key: "ldapUrl",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapBindDn: model.AppConfigVariable{
|
||||||
|
Key: "ldapBindDn",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapBindPassword: model.AppConfigVariable{
|
||||||
|
Key: "ldapBindPassword",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapBase: model.AppConfigVariable{
|
||||||
|
Key: "ldapBase",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapUserSearchFilter: model.AppConfigVariable{
|
||||||
|
Key: "ldapUserSearchFilter",
|
||||||
|
Type: "string",
|
||||||
|
DefaultValue: "(objectClass=person)",
|
||||||
|
},
|
||||||
|
LdapUserGroupSearchFilter: model.AppConfigVariable{
|
||||||
|
Key: "ldapUserGroupSearchFilter",
|
||||||
|
Type: "string",
|
||||||
|
DefaultValue: "(objectClass=groupOfNames)",
|
||||||
|
},
|
||||||
|
LdapSkipCertVerify: model.AppConfigVariable{
|
||||||
|
Key: "ldapSkipCertVerify",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
LdapAttributeUserUniqueIdentifier: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserUniqueIdentifier",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserUsername: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserUsername",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserEmail: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserEmail",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserFirstName: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserFirstName",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserLastName: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserLastName",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeGroupUniqueIdentifier",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeGroupName: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeGroupName",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeAdminGroup: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeAdminGroup",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) {
|
func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) {
|
||||||
var savedConfigVariables []model.AppConfigVariable
|
if common.EnvConfig.UiConfigDisabled {
|
||||||
|
return nil, &common.UiConfigDisabledError{}
|
||||||
|
}
|
||||||
|
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
rt := reflect.ValueOf(input).Type()
|
rt := reflect.ValueOf(input).Type()
|
||||||
rv := reflect.ValueOf(input)
|
rv := reflect.ValueOf(input)
|
||||||
|
|
||||||
|
var savedConfigVariables []model.AppConfigVariable
|
||||||
for i := 0; i < rt.NumField(); i++ {
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
field := rt.Field(i)
|
field := rt.Field(i)
|
||||||
key := field.Tag.Get("json")
|
key := field.Tag.Get("json")
|
||||||
value := rv.FieldByName(field.Name).String()
|
value := rv.FieldByName(field.Name).String()
|
||||||
|
|
||||||
|
// If the emailEnabled is set to false, disable the emailOneTimeAccessEnabled
|
||||||
|
if key == s.DbConfig.EmailOneTimeAccessEnabled.Key {
|
||||||
|
if rv.FieldByName("EmailEnabled").String() == "false" {
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var appConfigVariable model.AppConfigVariable
|
var appConfigVariable model.AppConfigVariable
|
||||||
if err := tx.First(&appConfigVariable, "key = ? AND is_internal = false", key).Error; err != nil {
|
if err := tx.First(&appConfigVariable, "key = ? AND is_internal = false", key).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -115,7 +226,7 @@ func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]mode
|
|||||||
|
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|
||||||
if err := s.loadDbConfigFromDb(); err != nil {
|
if err := s.LoadDbConfigFromDb(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +240,7 @@ func (s *AppConfigService) UpdateImageType(imageName string, fileType string) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.loadDbConfigFromDb()
|
return s.LoadDbConfigFromDb()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppConfigService) ListAppConfig(showAll bool) ([]model.AppConfigVariable, error) {
|
func (s *AppConfigService) ListAppConfig(showAll bool) ([]model.AppConfigVariable, error) {
|
||||||
@@ -146,6 +257,17 @@ func (s *AppConfigService) ListAppConfig(showAll bool) ([]model.AppConfigVariabl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range configuration {
|
||||||
|
if common.EnvConfig.UiConfigDisabled {
|
||||||
|
// Set the value to the environment variable if the UI config is disabled
|
||||||
|
configuration[i].Value = s.getConfigVariableFromEnvironmentVariable(configuration[i].Key, configuration[i].DefaultValue)
|
||||||
|
|
||||||
|
} else if configuration[i].Value == "" && configuration[i].DefaultValue != "" {
|
||||||
|
// Set the value to the default value if it is empty
|
||||||
|
configuration[i].Value = configuration[i].DefaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return configuration, nil
|
return configuration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +275,7 @@ func (s *AppConfigService) UpdateImage(uploadedFile *multipart.FileHeader, image
|
|||||||
fileType := utils.GetFileExtension(uploadedFile.Filename)
|
fileType := utils.GetFileExtension(uploadedFile.Filename)
|
||||||
mimeType := utils.GetImageMimeType(fileType)
|
mimeType := utils.GetImageMimeType(fileType)
|
||||||
if mimeType == "" {
|
if mimeType == "" {
|
||||||
return common.ErrFileTypeNotSupported
|
return &common.FileTypeNotSupportedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the old image if it has a different file type
|
// Delete the old image if it has a different file type
|
||||||
@@ -201,10 +323,11 @@ func (s *AppConfigService) InitDbConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update existing configuration if it differs from the default
|
// Update existing configuration if it differs from the default
|
||||||
if storedConfigVar.Type != defaultConfigVar.Type || storedConfigVar.IsPublic != defaultConfigVar.IsPublic || storedConfigVar.IsInternal != defaultConfigVar.IsInternal {
|
if storedConfigVar.Type != defaultConfigVar.Type || storedConfigVar.IsPublic != defaultConfigVar.IsPublic || storedConfigVar.IsInternal != defaultConfigVar.IsInternal || storedConfigVar.DefaultValue != defaultConfigVar.DefaultValue {
|
||||||
storedConfigVar.Type = defaultConfigVar.Type
|
storedConfigVar.Type = defaultConfigVar.Type
|
||||||
storedConfigVar.IsPublic = defaultConfigVar.IsPublic
|
storedConfigVar.IsPublic = defaultConfigVar.IsPublic
|
||||||
storedConfigVar.IsInternal = defaultConfigVar.IsInternal
|
storedConfigVar.IsInternal = defaultConfigVar.IsInternal
|
||||||
|
storedConfigVar.DefaultValue = defaultConfigVar.DefaultValue
|
||||||
if err := s.db.Save(&storedConfigVar).Error; err != nil {
|
if err := s.db.Save(&storedConfigVar).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -224,10 +347,11 @@ func (s *AppConfigService) InitDbConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.loadDbConfigFromDb()
|
return s.LoadDbConfigFromDb()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppConfigService) loadDbConfigFromDb() error {
|
// LoadDbConfigFromDb loads the configuration values from the database into the DbConfig struct.
|
||||||
|
func (s *AppConfigService) LoadDbConfigFromDb() error {
|
||||||
dbConfigReflectValue := reflect.ValueOf(s.DbConfig).Elem()
|
dbConfigReflectValue := reflect.ValueOf(s.DbConfig).Elem()
|
||||||
|
|
||||||
for i := 0; i < dbConfigReflectValue.NumField(); i++ {
|
for i := 0; i < dbConfigReflectValue.NumField(); i++ {
|
||||||
@@ -238,8 +362,25 @@ func (s *AppConfigService) loadDbConfigFromDb() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if common.EnvConfig.UiConfigDisabled {
|
||||||
|
storedConfigVar.Value = s.getConfigVariableFromEnvironmentVariable(currentConfigVar.Key, storedConfigVar.DefaultValue)
|
||||||
|
} else if storedConfigVar.Value == "" && storedConfigVar.DefaultValue != "" {
|
||||||
|
storedConfigVar.Value = storedConfigVar.DefaultValue
|
||||||
|
}
|
||||||
|
|
||||||
dbConfigField.Set(reflect.ValueOf(storedConfigVar))
|
dbConfigField.Set(reflect.ValueOf(storedConfigVar))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AppConfigService) getConfigVariableFromEnvironmentVariable(key, fallbackValue string) string {
|
||||||
|
environmentVariableName := utils.CamelCaseToScreamingSnakeCase(key)
|
||||||
|
|
||||||
|
if value, exists := os.LookupEnv(environmentVariableName); exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackValue
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
userAgentParser "github.com/mileusna/useragent"
|
|
||||||
"github.com/oschwald/maxminddb-golang/v2"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"log"
|
"log"
|
||||||
"net/netip"
|
|
||||||
|
userAgentParser "github.com/mileusna/useragent"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuditLogService struct {
|
type AuditLogService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
emailService *EmailService
|
emailService *EmailService
|
||||||
|
geoliteService *GeoLiteService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuditLogService(db *gorm.DB, appConfigService *AppConfigService, emailService *EmailService) *AuditLogService {
|
func NewAuditLogService(db *gorm.DB, appConfigService *AppConfigService, emailService *EmailService, geoliteService *GeoLiteService) *AuditLogService {
|
||||||
return &AuditLogService{db: db, appConfigService: appConfigService, emailService: emailService}
|
return &AuditLogService{db: db, appConfigService: appConfigService, emailService: emailService, geoliteService: geoliteService}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new audit log entry in the database
|
// Create creates a new audit log entry in the database
|
||||||
func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
|
func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
|
||||||
country, city, err := s.GetIpLocation(ipAddress)
|
country, city, err := s.geoliteService.GetLocationByIP(ipAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get IP location: %v\n", err)
|
log.Printf("Failed to get IP location: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,8 @@ func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateNewSignInWithEmail creates a new audit log entry in the database and sends an email if the device hasn't been used before
|
// CreateNewSignInWithEmail creates a new audit log entry in the database and sends an email if the device hasn't been used before
|
||||||
func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
|
func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID string) model.AuditLog {
|
||||||
createdAuditLog := s.Create(model.AuditLogEventSignIn, ipAddress, userAgent, userID, data)
|
createdAuditLog := s.Create(model.AuditLogEventSignIn, ipAddress, userAgent, userID, model.AuditLogData{})
|
||||||
|
|
||||||
// Count the number of times the user has logged in from the same device
|
// Count the number of times the user has logged in from the same device
|
||||||
var count int64
|
var count int64
|
||||||
@@ -59,8 +59,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
|
|||||||
return createdAuditLog
|
return createdAuditLog
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user hasn't logged in from the same device before, send an email
|
// If the user hasn't logged in from the same device before and email notifications are enabled, send an email
|
||||||
if count <= 1 {
|
if s.appConfigService.DbConfig.EmailLoginNotificationEnabled.Value == "true" && count <= 1 {
|
||||||
go func() {
|
go func() {
|
||||||
var user model.User
|
var user model.User
|
||||||
s.db.Where("id = ?", userID).First(&user)
|
s.db.Where("id = ?", userID).First(&user)
|
||||||
@@ -85,11 +85,11 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListAuditLogsForUser retrieves all audit logs for a given user ID
|
// ListAuditLogsForUser retrieves all audit logs for a given user ID
|
||||||
func (s *AuditLogService) ListAuditLogsForUser(userID string, page int, pageSize int) ([]model.AuditLog, utils.PaginationResponse, error) {
|
func (s *AuditLogService) ListAuditLogsForUser(userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.AuditLog, utils.PaginationResponse, error) {
|
||||||
var logs []model.AuditLog
|
var logs []model.AuditLog
|
||||||
query := s.db.Model(&model.AuditLog{}).Where("user_id = ?", userID).Order("created_at desc")
|
query := s.db.Model(&model.AuditLog{}).Where("user_id = ?", userID)
|
||||||
|
|
||||||
pagination, err := utils.Paginate(page, pageSize, query, &logs)
|
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
|
||||||
return logs, pagination, err
|
return logs, pagination, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,29 +97,3 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
|
|||||||
ua := userAgentParser.Parse(userAgent)
|
ua := userAgentParser.Parse(userAgent)
|
||||||
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuditLogService) GetIpLocation(ipAddress string) (country, city string, err error) {
|
|
||||||
db, err := maxminddb.Open("GeoLite2-City.mmdb")
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
addr := netip.MustParseAddr(ipAddress)
|
|
||||||
|
|
||||||
var record struct {
|
|
||||||
City struct {
|
|
||||||
Names map[string]string `maxminddb:"names"`
|
|
||||||
} `maxminddb:"city"`
|
|
||||||
Country struct {
|
|
||||||
Names map[string]string `maxminddb:"names"`
|
|
||||||
} `maxminddb:"country"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Lookup(addr).Decode(&record)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.Country.Names["en"], record.City.Names["en"], nil
|
|
||||||
}
|
|
||||||
|
|||||||
197
backend/internal/service/custom_claim_service.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reserved claims
|
||||||
|
var reservedClaims = map[string]struct{}{
|
||||||
|
"given_name": {},
|
||||||
|
"family_name": {},
|
||||||
|
"name": {},
|
||||||
|
"email": {},
|
||||||
|
"preferred_username": {},
|
||||||
|
"groups": {},
|
||||||
|
"sub": {},
|
||||||
|
"iss": {},
|
||||||
|
"aud": {},
|
||||||
|
"exp": {},
|
||||||
|
"iat": {},
|
||||||
|
"auth_time": {},
|
||||||
|
"nonce": {},
|
||||||
|
"acr": {},
|
||||||
|
"amr": {},
|
||||||
|
"azp": {},
|
||||||
|
"nbf": {},
|
||||||
|
"jti": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomClaimService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomClaimService(db *gorm.DB) *CustomClaimService {
|
||||||
|
return &CustomClaimService{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isReservedClaim checks if a claim key is reserved e.g. email, preferred_username
|
||||||
|
func isReservedClaim(key string) bool {
|
||||||
|
_, ok := reservedClaims[key]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// idType is the type of the id used to identify the user or user group
|
||||||
|
type idType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserID idType = "user_id"
|
||||||
|
UserGroupID idType = "user_group_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateCustomClaimsForUser updates the custom claims for a user
|
||||||
|
func (s *CustomClaimService) UpdateCustomClaimsForUser(userID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
||||||
|
return s.updateCustomClaims(UserID, userID, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCustomClaimsForUserGroup updates the custom claims for a user group
|
||||||
|
func (s *CustomClaimService) UpdateCustomClaimsForUserGroup(userGroupID string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
||||||
|
return s.updateCustomClaims(UserGroupID, userGroupID, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCustomClaims updates the custom claims for a user or user group
|
||||||
|
func (s *CustomClaimService) updateCustomClaims(idType idType, value string, claims []dto.CustomClaimCreateDto) ([]model.CustomClaim, error) {
|
||||||
|
// Check for duplicate keys in the claims slice
|
||||||
|
seenKeys := make(map[string]bool)
|
||||||
|
for _, claim := range claims {
|
||||||
|
if seenKeys[claim.Key] {
|
||||||
|
return nil, &common.DuplicateClaimError{Key: claim.Key}
|
||||||
|
}
|
||||||
|
seenKeys[claim.Key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingClaims []model.CustomClaim
|
||||||
|
err := s.db.Where(string(idType), value).Find(&existingClaims).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete claims that are not in the new list
|
||||||
|
for _, existingClaim := range existingClaims {
|
||||||
|
found := false
|
||||||
|
for _, claim := range claims {
|
||||||
|
if claim.Key == existingClaim.Key {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
err = s.db.Delete(&existingClaim).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or update claims
|
||||||
|
for _, claim := range claims {
|
||||||
|
if isReservedClaim(claim.Key) {
|
||||||
|
return nil, &common.ReservedClaimError{Key: claim.Key}
|
||||||
|
}
|
||||||
|
customClaim := model.CustomClaim{
|
||||||
|
Key: claim.Key,
|
||||||
|
Value: claim.Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if idType == UserID {
|
||||||
|
customClaim.UserID = &value
|
||||||
|
} else if idType == UserGroupID {
|
||||||
|
customClaim.UserGroupID = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the claim if it already exists or create a new one
|
||||||
|
err = s.db.Where(string(idType)+" = ? AND key = ?", value, claim.Key).Assign(&customClaim).FirstOrCreate(&model.CustomClaim{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the updated claims
|
||||||
|
var updatedClaims []model.CustomClaim
|
||||||
|
err = s.db.Where(string(idType)+" = ?", value).Find(&updatedClaims).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedClaims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CustomClaimService) GetCustomClaimsForUser(userID string) ([]model.CustomClaim, error) {
|
||||||
|
var customClaims []model.CustomClaim
|
||||||
|
err := s.db.Where("user_id = ?", userID).Find(&customClaims).Error
|
||||||
|
return customClaims, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CustomClaimService) GetCustomClaimsForUserGroup(userGroupID string) ([]model.CustomClaim, error) {
|
||||||
|
var customClaims []model.CustomClaim
|
||||||
|
err := s.db.Where("user_group_id = ?", userGroupID).Find(&customClaims).Error
|
||||||
|
return customClaims, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCustomClaimsForUserWithUserGroups returns the custom claims of a user and all user groups the user is a member of,
|
||||||
|
// prioritizing the user's claims over user group claims with the same key.
|
||||||
|
func (s *CustomClaimService) GetCustomClaimsForUserWithUserGroups(userID string) ([]model.CustomClaim, error) {
|
||||||
|
// Get the custom claims of the user
|
||||||
|
customClaims, err := s.GetCustomClaimsForUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user's claims in a map to prioritize and prevent duplicates
|
||||||
|
claimsMap := make(map[string]model.CustomClaim)
|
||||||
|
for _, claim := range customClaims {
|
||||||
|
claimsMap[claim.Key] = claim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all user groups of the user
|
||||||
|
var userGroupsOfUser []model.UserGroup
|
||||||
|
err = s.db.Preload("CustomClaims").
|
||||||
|
Joins("JOIN user_groups_users ON user_groups_users.user_group_id = user_groups.id").
|
||||||
|
Where("user_groups_users.user_id = ?", userID).
|
||||||
|
Find(&userGroupsOfUser).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add only non-duplicate custom claims from user groups
|
||||||
|
for _, userGroup := range userGroupsOfUser {
|
||||||
|
for _, groupClaim := range userGroup.CustomClaims {
|
||||||
|
// Only add claim if it does not exist in the user's claims
|
||||||
|
if _, exists := claimsMap[groupClaim.Key]; !exists {
|
||||||
|
claimsMap[groupClaim.Key] = groupClaim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the claimsMap back to a slice
|
||||||
|
finalClaims := make([]model.CustomClaim, 0, len(claimsMap))
|
||||||
|
for _, claim := range claimsMap {
|
||||||
|
finalClaims = append(finalClaims, claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalClaims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSuggestions returns a list of custom claim keys that have been used before
|
||||||
|
func (s *CustomClaimService) GetSuggestions() ([]string, error) {
|
||||||
|
var customClaimsKeys []string
|
||||||
|
|
||||||
|
err := s.db.Model(&model.CustomClaim{}).
|
||||||
|
Group("key").
|
||||||
|
Order("COUNT(*) DESC").
|
||||||
|
Pluck("key", &customClaimsKeys).Error
|
||||||
|
|
||||||
|
return customClaimsKeys, err
|
||||||
|
}
|
||||||
@@ -2,49 +2,68 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
|
||||||
htemplate "html/template"
|
htemplate "html/template"
|
||||||
"io/fs"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
"mime/quotedprintable"
|
||||||
|
"net"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"os"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var netDialer = &net.Dialer{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
type EmailService struct {
|
type EmailService struct {
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
|
db *gorm.DB
|
||||||
htmlTemplates map[string]*htemplate.Template
|
htmlTemplates map[string]*htemplate.Template
|
||||||
textTemplates map[string]*ttemplate.Template
|
textTemplates map[string]*ttemplate.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmailService(appConfigService *AppConfigService, templateDir fs.FS) (*EmailService, error) {
|
func NewEmailService(appConfigService *AppConfigService, db *gorm.DB) (*EmailService, error) {
|
||||||
htmlTemplates, err := email.PrepareHTMLTemplates(templateDir, emailTemplatesPaths)
|
htmlTemplates, err := email.PrepareHTMLTemplates(emailTemplatesPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare html templates: %w", err)
|
return nil, fmt.Errorf("prepare html templates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
textTemplates, err := email.PrepareTextTemplates(templateDir, emailTemplatesPaths)
|
textTemplates, err := email.PrepareTextTemplates(emailTemplatesPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare html templates: %w", err)
|
return nil, fmt.Errorf("prepare html templates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &EmailService{
|
return &EmailService{
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
|
db: db,
|
||||||
htmlTemplates: htmlTemplates,
|
htmlTemplates: htmlTemplates,
|
||||||
textTemplates: textTemplates,
|
textTemplates: textTemplates,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
func (srv *EmailService) SendTestEmail(recipientUserId string) error {
|
||||||
// Check if SMTP settings are set
|
var user model.User
|
||||||
if srv.appConfigService.DbConfig.EmailEnabled.Value != "true" {
|
if err := srv.db.First(&user, "id = ?", recipientUserId).Error; err != nil {
|
||||||
return errors.New("email not enabled")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return SendEmail(srv,
|
||||||
|
email.Address{
|
||||||
|
Email: user.Email,
|
||||||
|
Name: user.FullName(),
|
||||||
|
}, TestTemplate, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
||||||
data := &email.TemplateData[V]{
|
data := &email.TemplateData[V]{
|
||||||
AppName: srv.appConfigService.DbConfig.AppName.Value,
|
AppName: srv.appConfigService.DbConfig.AppName.Value,
|
||||||
LogoURL: common.EnvConfig.AppURL + "/api/application-configuration/logo",
|
LogoURL: common.EnvConfig.AppURL + "/api/application-configuration/logo",
|
||||||
@@ -71,29 +90,160 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
)
|
)
|
||||||
c.Body(body)
|
c.Body(body)
|
||||||
|
|
||||||
// Set up the authentication information.
|
// Connect to the SMTP server
|
||||||
auth := smtp.PlainAuth("",
|
client, err := srv.getSmtpClient()
|
||||||
srv.appConfigService.DbConfig.SmtpUser.Value,
|
if err != nil {
|
||||||
srv.appConfigService.DbConfig.SmtpPassword.Value,
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
srv.appConfigService.DbConfig.SmtpHost.Value,
|
}
|
||||||
)
|
defer client.Close()
|
||||||
|
|
||||||
// Send the email
|
// Send the email
|
||||||
err = smtp.SendMail(
|
if err := srv.sendEmailContent(client, toEmail, c); err != nil {
|
||||||
srv.appConfigService.DbConfig.SmtpHost.Value+":"+srv.appConfigService.DbConfig.SmtpPort.Value,
|
return fmt.Errorf("send email content: %w", err)
|
||||||
auth,
|
|
||||||
srv.appConfigService.DbConfig.SmtpFrom.Value,
|
|
||||||
[]string{toEmail.Email},
|
|
||||||
[]byte(c.String()),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send email: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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{
|
||||||
|
InsecureSkipVerify: srv.appConfigService.DbConfig.SmtpSkipCertVerify.Value == "true",
|
||||||
|
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the SMTP server
|
||||||
|
if srv.appConfigService.DbConfig.SmtpTls.Value == "false" {
|
||||||
|
client, err = srv.connectToSmtpServer(smtpAddress)
|
||||||
|
} else if port == "465" {
|
||||||
|
client, err = srv.connectToSmtpServerUsingImplicitTLS(
|
||||||
|
smtpAddress,
|
||||||
|
tlsConfig,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
client, err = srv.connectToSmtpServerUsingStartTLS(
|
||||||
|
smtpAddress,
|
||||||
|
tlsConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the authentication if user or password are set
|
||||||
|
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
||||||
|
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
||||||
|
|
||||||
|
if smtpUser != "" || smtpPassword != "" {
|
||||||
|
auth := smtp.PlainAuth("",
|
||||||
|
srv.appConfigService.DbConfig.SmtpUser.Value,
|
||||||
|
srv.appConfigService.DbConfig.SmtpPassword.Value,
|
||||||
|
srv.appConfigService.DbConfig.SmtpHost.Value,
|
||||||
|
)
|
||||||
|
if err := client.Auth(auth); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to authenticate SMTP client: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
tlsDialer := &tls.Dialer{
|
||||||
|
NetDialer: netDialer,
|
||||||
|
Config: tlsConfig,
|
||||||
|
}
|
||||||
|
conn, err := tlsDialer.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.sendHelloCommand(client); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value); err != nil {
|
||||||
|
return fmt.Errorf("failed to set sender: %w", err)
|
||||||
|
}
|
||||||
|
if err := client.Rcpt(toEmail.Email); err != nil {
|
||||||
|
return fmt.Errorf("failed to set recipient: %w", err)
|
||||||
|
}
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start data: %w", err)
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte(c.String()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write email data: %w", err)
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close data writer: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func prepareBody[V any](srv *EmailService, template email.Template[V], data *email.TemplateData[V]) (string, string, error) {
|
func prepareBody[V any](srv *EmailService, template email.Template[V], data *email.TemplateData[V]) (string, string, error) {
|
||||||
body := bytes.NewBuffer(nil)
|
body := bytes.NewBuffer(nil)
|
||||||
mpart := multipart.NewWriter(body)
|
mpart := multipart.NewWriter(body)
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
How to add new template:
|
How to add new template:
|
||||||
- pick unique and descriptive template ${name} (for example "login-with-new-device")
|
- pick unique and descriptive template ${name} (for example "login-with-new-device")
|
||||||
- in backend/email-templates/ create "${name}_html.tmpl" and "${name}_text.tmpl"
|
- in backend/resources/email-templates/ create "${name}_html.tmpl" and "${name}_text.tmpl"
|
||||||
- create xxxxTemplate and xxxxTemplateData (for example NewLoginTemplate and NewLoginTemplateData)
|
- create xxxxTemplate and xxxxTemplateData (for example NewLoginTemplate and NewLoginTemplateData)
|
||||||
- Path *must* be ${name}
|
- Path *must* be ${name}
|
||||||
- add xxxTemplate.Path to "emailTemplatePaths" at the end
|
- add xxxTemplate.Path to "emailTemplatePaths" at the end
|
||||||
@@ -27,6 +28,20 @@ var NewLoginTemplate = email.Template[NewLoginTemplateData]{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var OneTimeAccessTemplate = email.Template[OneTimeAccessTemplateData]{
|
||||||
|
Path: "one-time-access",
|
||||||
|
Title: func(data *email.TemplateData[OneTimeAccessTemplateData]) string {
|
||||||
|
return "One time access"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var TestTemplate = email.Template[struct{}]{
|
||||||
|
Path: "test",
|
||||||
|
Title: func(data *email.TemplateData[struct{}]) string {
|
||||||
|
return "Test email"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
type NewLoginTemplateData struct {
|
type NewLoginTemplateData struct {
|
||||||
IPAddress string
|
IPAddress string
|
||||||
Country string
|
Country string
|
||||||
@@ -35,5 +50,9 @@ type NewLoginTemplateData struct {
|
|||||||
DateTime time.Time
|
DateTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OneTimeAccessTemplateData = struct {
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
// this is list of all template paths used for preloading templates
|
// this is list of all template paths used for preloading templates
|
||||||
var emailTemplatesPaths = []string{NewLoginTemplate.Path}
|
var emailTemplatesPaths = []string{NewLoginTemplate.Path, OneTimeAccessTemplate.Path, TestTemplate.Path}
|
||||||
|
|||||||
214
backend/internal/service/geolite_service.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oschwald/maxminddb-golang/v2"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeoLiteService struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var localhostIPNets = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
|
||||||
|
{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, // ::1/128
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateLanIPNets = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 10.0.0.0/8
|
||||||
|
{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // 172.16.0.0/12
|
||||||
|
{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // 192.168.0.0/16
|
||||||
|
}
|
||||||
|
|
||||||
|
var tailscaleIPNets = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}, // 100.64.0.0/10
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGeoLiteService initializes a new GeoLiteService instance and starts a goroutine to update the GeoLite2 City database.
|
||||||
|
func NewGeoLiteService() *GeoLiteService {
|
||||||
|
service := &GeoLiteService{}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := service.updateDatabase(); err != nil {
|
||||||
|
log.Printf("Failed to update GeoLite2 City database: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocationByIP returns the country and city of the given IP address.
|
||||||
|
func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string, err error) {
|
||||||
|
// Check the IP address against known private IP ranges
|
||||||
|
if ip := net.ParseIP(ipAddress); ip != nil {
|
||||||
|
for _, ipNet := range tailscaleIPNets {
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
return "Internal Network", "Tailscale", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ipNet := range privateLanIPNets {
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
return "Internal Network", "LAN/Docker/k8s", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ipNet := range localhostIPNets {
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
return "Internal Network", "localhost", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Race condition between reading and writing the database.
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
db, err := maxminddb.Open(common.EnvConfig.GeoLiteDBPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
addr := netip.MustParseAddr(ipAddress)
|
||||||
|
|
||||||
|
var record struct {
|
||||||
|
City struct {
|
||||||
|
Names map[string]string `maxminddb:"names"`
|
||||||
|
} `maxminddb:"city"`
|
||||||
|
Country struct {
|
||||||
|
Names map[string]string `maxminddb:"names"`
|
||||||
|
} `maxminddb:"country"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Lookup(addr).Decode(&record)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.Country.Names["en"], record.City.Names["en"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDatabase checks the age of the database and updates it if it's older than 14 days.
|
||||||
|
func (s *GeoLiteService) updateDatabase() error {
|
||||||
|
if s.isDatabaseUpToDate() {
|
||||||
|
log.Println("GeoLite2 City database is up-to-date.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Updating GeoLite2 City database...")
|
||||||
|
|
||||||
|
// Download and extract the database
|
||||||
|
downloadUrl := fmt.Sprintf(
|
||||||
|
"https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz",
|
||||||
|
common.EnvConfig.MaxMindLicenseKey,
|
||||||
|
)
|
||||||
|
// Download the database tar.gz file
|
||||||
|
resp, err := http.Get(downloadUrl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download database: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to download database, received HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the database file directly to the target path
|
||||||
|
if err := s.extractDatabase(resp.Body); err != nil {
|
||||||
|
return fmt.Errorf("failed to extract database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("GeoLite2 City database successfully updated.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDatabaseUpToDate checks if the database file is older than 14 days.
|
||||||
|
func (s *GeoLiteService) isDatabaseUpToDate() bool {
|
||||||
|
info, err := os.Stat(common.EnvConfig.GeoLiteDBPath)
|
||||||
|
if err != nil {
|
||||||
|
// If the file doesn't exist, treat it as not up-to-date
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Since(info.ModTime()) < 14*24*time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractDatabase extracts the database file from the tar.gz archive directly to the target location.
|
||||||
|
func (s *GeoLiteService) extractDatabase(reader io.Reader) error {
|
||||||
|
gzr, err := gzip.NewReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create gzip reader: %w", err)
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(gzr)
|
||||||
|
|
||||||
|
// Iterate over the files in the tar archive
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read tar archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file is the GeoLite2-City.mmdb file
|
||||||
|
if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "GeoLite2-City.mmdb" {
|
||||||
|
// extract to a temporary file to avoid having a corrupted db in case of write failure.
|
||||||
|
baseDir := filepath.Dir(common.EnvConfig.GeoLiteDBPath)
|
||||||
|
tmpFile, err := os.CreateTemp(baseDir, "geolite.*.mmdb.tmp")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temporary database file: %w", err)
|
||||||
|
}
|
||||||
|
tempName := tmpFile.Name()
|
||||||
|
|
||||||
|
// Write the file contents directly to the target location
|
||||||
|
if _, err := io.Copy(tmpFile, tarReader); err != nil {
|
||||||
|
// if fails to write, then cleanup and throw an error
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tempName)
|
||||||
|
return fmt.Errorf("failed to write database file: %w", err)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
// ensure the database is not corrupted
|
||||||
|
db, err := maxminddb.Open(tempName)
|
||||||
|
if err != nil {
|
||||||
|
// if fails to write, then cleanup and throw an error
|
||||||
|
os.Remove(tempName)
|
||||||
|
return fmt.Errorf("failed to open downloaded database file: %w", err)
|
||||||
|
}
|
||||||
|
db.Close()
|
||||||
|
|
||||||
|
// ensure we lock the structure before we overwrite the database
|
||||||
|
// to prevent race conditions between reading and writing the mmdb.
|
||||||
|
s.mutex.Lock()
|
||||||
|
// replace the old file with the new file
|
||||||
|
err = os.Rename(tempName, common.EnvConfig.GeoLiteDBPath)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// if cannot overwrite via rename, then cleanup and throw an error
|
||||||
|
os.Remove(tempName)
|
||||||
|
return fmt.Errorf("failed to replace database file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("GeoLite2-City.mmdb not found in archive")
|
||||||
|
}
|
||||||
@@ -9,10 +9,6 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
@@ -20,6 +16,10 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -96,7 +96,7 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
|||||||
Subject: user.ID,
|
Subject: user.ID,
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(sessionDurationInMinutes) * time.Minute)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(sessionDurationInMinutes) * time.Minute)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
Audience: jwt.ClaimStrings{utils.GetHostFromURL(common.EnvConfig.AppURL)},
|
Audience: jwt.ClaimStrings{common.EnvConfig.AppURL},
|
||||||
},
|
},
|
||||||
IsAdmin: user.IsAdmin,
|
IsAdmin: user.IsAdmin,
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ func (s *JwtService) VerifyAccessToken(tokenString string) (*AccessTokenJWTClaim
|
|||||||
return nil, errors.New("can't parse claims")
|
return nil, errors.New("can't parse claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(claims.Audience, utils.GetHostFromURL(common.EnvConfig.AppURL)) {
|
if !slices.Contains(claims.Audience, common.EnvConfig.AppURL) {
|
||||||
return nil, errors.New("audience doesn't match")
|
return nil, errors.New("audience doesn't match")
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
|
|||||||
261
backend/internal/service/ldap_service.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LdapService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
appConfigService *AppConfigService
|
||||||
|
userService *UserService
|
||||||
|
groupService *UserGroupService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLdapService(db *gorm.DB, appConfigService *AppConfigService, userService *UserService, groupService *UserGroupService) *LdapService {
|
||||||
|
return &LdapService{db: db, appConfigService: appConfigService, userService: userService, groupService: groupService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) createClient() (*ldap.Conn, error) {
|
||||||
|
if s.appConfigService.DbConfig.LdapEnabled.Value != "true" {
|
||||||
|
return nil, fmt.Errorf("LDAP is not enabled")
|
||||||
|
}
|
||||||
|
// Setup LDAP connection
|
||||||
|
ldapURL := s.appConfigService.DbConfig.LdapUrl.Value
|
||||||
|
skipTLSVerify := s.appConfigService.DbConfig.LdapSkipCertVerify.Value == "true"
|
||||||
|
client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: skipTLSVerify}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to LDAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind as service account
|
||||||
|
bindDn := s.appConfigService.DbConfig.LdapBindDn.Value
|
||||||
|
bindPassword := s.appConfigService.DbConfig.LdapBindPassword.Value
|
||||||
|
err = client.Bind(bindDn, bindPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to bind to LDAP: %w", err)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SyncAll() error {
|
||||||
|
err := s.SyncUsers()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sync users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.SyncGroups()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sync groups: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SyncGroups() error {
|
||||||
|
// Setup LDAP connection
|
||||||
|
client, err := s.createClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create LDAP client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
baseDN := s.appConfigService.DbConfig.LdapBase.Value
|
||||||
|
nameAttribute := s.appConfigService.DbConfig.LdapAttributeGroupName.Value
|
||||||
|
uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeGroupUniqueIdentifier.Value
|
||||||
|
filter := s.appConfigService.DbConfig.LdapUserGroupSearchFilter.Value
|
||||||
|
|
||||||
|
searchAttrs := []string{
|
||||||
|
nameAttribute,
|
||||||
|
uniqueIdentifierAttribute,
|
||||||
|
"member",
|
||||||
|
}
|
||||||
|
|
||||||
|
searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{})
|
||||||
|
result, err := client.Search(searchReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query LDAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mapping for groups that exist
|
||||||
|
ldapGroupIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, value := range result.Entries {
|
||||||
|
var usersToAddDto dto.UserGroupUpdateUsersDto
|
||||||
|
var membersUserId []string
|
||||||
|
|
||||||
|
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||||
|
ldapGroupIDs[ldapId] = true
|
||||||
|
|
||||||
|
// Try to find the group in the database
|
||||||
|
var databaseGroup model.UserGroup
|
||||||
|
s.db.Where("ldap_id = ?", ldapId).First(&databaseGroup)
|
||||||
|
|
||||||
|
// Get group members and add to the correct Group
|
||||||
|
groupMembers := value.GetAttributeValues("member")
|
||||||
|
for _, member := range groupMembers {
|
||||||
|
// Normal output of this would be CN=username,ou=people,dc=example,dc=com
|
||||||
|
// Splitting at the "=" and "," then just grabbing the username for that string
|
||||||
|
singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]
|
||||||
|
|
||||||
|
var databaseUser model.User
|
||||||
|
s.db.Where("username = ?", singleMember).Where("ldap_id IS NOT NULL").First(&databaseUser)
|
||||||
|
|
||||||
|
membersUserId = append(membersUserId, databaseUser.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncGroup := dto.UserGroupCreateDto{
|
||||||
|
Name: value.GetAttributeValue(nameAttribute),
|
||||||
|
FriendlyName: value.GetAttributeValue(nameAttribute),
|
||||||
|
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
|
||||||
|
}
|
||||||
|
|
||||||
|
usersToAddDto = dto.UserGroupUpdateUsersDto{
|
||||||
|
UserIDs: membersUserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if databaseGroup.ID == "" {
|
||||||
|
newGroup, err := s.groupService.Create(syncGroup)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
|
} else {
|
||||||
|
if _, err = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto); err != nil {
|
||||||
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = s.groupService.Update(databaseGroup.ID, syncGroup, true)
|
||||||
|
_, err = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all LDAP groups from the database
|
||||||
|
var ldapGroupsInDb []model.UserGroup
|
||||||
|
if err := s.db.Find(&ldapGroupsInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to fetch groups from database: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete groups that no longer exist in LDAP
|
||||||
|
for _, group := range ldapGroupsInDb {
|
||||||
|
if _, exists := ldapGroupIDs[*group.LdapID]; !exists {
|
||||||
|
if err := s.db.Delete(&model.UserGroup{}, "ldap_id = ?", group.LdapID).Error; err != nil {
|
||||||
|
log.Printf("Failed to delete group %s with: %v", group.Name, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Deleted group %s", group.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SyncUsers() error {
|
||||||
|
// Setup LDAP connection
|
||||||
|
client, err := s.createClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create LDAP client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
baseDN := s.appConfigService.DbConfig.LdapBase.Value
|
||||||
|
uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeUserUniqueIdentifier.Value
|
||||||
|
usernameAttribute := s.appConfigService.DbConfig.LdapAttributeUserUsername.Value
|
||||||
|
emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value
|
||||||
|
firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value
|
||||||
|
lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value
|
||||||
|
adminGroupAttribute := s.appConfigService.DbConfig.LdapAttributeAdminGroup.Value
|
||||||
|
filter := s.appConfigService.DbConfig.LdapUserSearchFilter.Value
|
||||||
|
|
||||||
|
searchAttrs := []string{
|
||||||
|
"memberOf",
|
||||||
|
"sn",
|
||||||
|
"cn",
|
||||||
|
uniqueIdentifierAttribute,
|
||||||
|
usernameAttribute,
|
||||||
|
emailAttribute,
|
||||||
|
firstNameAttribute,
|
||||||
|
lastNameAttribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters must start and finish with ()!
|
||||||
|
searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{})
|
||||||
|
|
||||||
|
result, err := client.Search(searchReq)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to query LDAP: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mapping for users that exist
|
||||||
|
ldapUserIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, value := range result.Entries {
|
||||||
|
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||||
|
ldapUserIDs[ldapId] = true
|
||||||
|
|
||||||
|
// Get the user from the database
|
||||||
|
var databaseUser model.User
|
||||||
|
s.db.Where("ldap_id = ?", ldapId).First(&databaseUser)
|
||||||
|
|
||||||
|
// Check if user is admin by checking if they are in the admin group
|
||||||
|
isAdmin := false
|
||||||
|
for _, group := range value.GetAttributeValues("memberOf") {
|
||||||
|
if strings.Contains(group, adminGroupAttribute) {
|
||||||
|
isAdmin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser := dto.UserCreateDto{
|
||||||
|
Username: value.GetAttributeValue(usernameAttribute),
|
||||||
|
Email: value.GetAttributeValue(emailAttribute),
|
||||||
|
FirstName: value.GetAttributeValue(firstNameAttribute),
|
||||||
|
LastName: value.GetAttributeValue(lastNameAttribute),
|
||||||
|
IsAdmin: isAdmin,
|
||||||
|
LdapID: ldapId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if databaseUser.ID == "" {
|
||||||
|
_, err = s.userService.CreateUser(newUser)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing user %s: %s", newUser.Username, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = s.userService.UpdateUser(databaseUser.ID, newUser, false, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing user %s: %s", newUser.Username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all LDAP users from the database
|
||||||
|
var ldapUsersInDb []model.User
|
||||||
|
if err := s.db.Find(&ldapUsersInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to fetch users from database: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete users that no longer exist in LDAP
|
||||||
|
for _, user := range ldapUsersInDb {
|
||||||
|
if _, exists := ldapUserIDs[*user.LdapID]; !exists {
|
||||||
|
if err := s.db.Delete(&model.User{}, "ldap_id = ?", user.LdapID).Error; err != nil {
|
||||||
|
log.Printf("Failed to delete user %s with: %v", user.Username, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Deleted user %s", user.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,103 +1,153 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
datatype "github.com/stonith404/pocket-id/backend/internal/model/types"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OidcService struct {
|
type OidcService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
jwtService *JwtService
|
jwtService *JwtService
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
auditLogService *AuditLogService
|
auditLogService *AuditLogService
|
||||||
|
customClaimService *CustomClaimService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOidcService(db *gorm.DB, jwtService *JwtService, appConfigService *AppConfigService, auditLogService *AuditLogService) *OidcService {
|
func NewOidcService(db *gorm.DB, jwtService *JwtService, appConfigService *AppConfigService, auditLogService *AuditLogService, customClaimService *CustomClaimService) *OidcService {
|
||||||
return &OidcService{
|
return &OidcService{
|
||||||
db: db,
|
db: db,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
auditLogService: auditLogService,
|
auditLogService: auditLogService,
|
||||||
|
customClaimService: customClaimService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Scope != input.Scope {
|
|
||||||
return "", "", common.ErrOidcMissingAuthorization
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackURL, err := getCallbackURL(userAuthorizedOIDCClient.Client, input.CallbackURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce)
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
callbackURL, err := getCallbackURL(client, input.CallbackURL)
|
// If the client is not public, the code challenge must be provided
|
||||||
|
if client.IsPublic && input.CodeChallenge == "" {
|
||||||
|
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)
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce)
|
// Create the authorization code
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret string) (string, string, error) {
|
// HasAuthorizedClient checks if the user has already authorized the client with the given scope
|
||||||
if grantType != "authorization_code" {
|
func (s *OidcService) HasAuthorizedClient(clientID, userID, scope string) (bool, error) {
|
||||||
return "", "", common.ErrOidcGrantTypeNotSupported
|
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 clientID == "" || clientSecret == "" {
|
if userAuthorizedOidcClient.Scope != scope {
|
||||||
return "", "", common.ErrOidcMissingClientCredentials
|
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) {
|
||||||
|
if grantType != "authorization_code" {
|
||||||
|
return "", "", &common.OidcGrantTypeNotSupportedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var client model.OidcClient
|
var client model.OidcClient
|
||||||
@@ -105,19 +155,33 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret strin
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret))
|
// Verify the client secret if the client is not public
|
||||||
if err != nil {
|
if !client.IsPublic {
|
||||||
return "", "", common.ErrOidcClientSecretInvalid
|
if clientID == "" || clientSecret == "" {
|
||||||
|
return "", "", &common.OidcMissingClientCredentialsError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", &common.OidcClientSecretInvalidError{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authorizationCodeMetaData model.OidcAuthorizationCode
|
var authorizationCodeMetaData model.OidcAuthorizationCode
|
||||||
err = s.db.Preload("User").First(&authorizationCodeMetaData, "code = ?", code).Error
|
err := s.db.Preload("User").First(&authorizationCodeMetaData, "code = ?", code).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", common.ErrOidcInvalidAuthorizationCode
|
return "", "", &common.OidcInvalidAuthorizationCodeError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the client is public or PKCE is enabled, the code verifier must match the code challenge
|
||||||
|
if client.IsPublic || client.PkceEnabled {
|
||||||
|
if !s.validateCodeVerifier(codeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
||||||
|
return "", "", &common.OidcInvalidCodeVerifierError{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) {
|
if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) {
|
||||||
return "", "", common.ErrOidcInvalidAuthorizationCode
|
return "", "", &common.OidcInvalidAuthorizationCodeError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
userClaims, err := s.GetUserClaimsForClient(authorizationCodeMetaData.UserID, clientID)
|
userClaims, err := s.GetUserClaimsForClient(authorizationCodeMetaData.UserID, clientID)
|
||||||
@@ -139,13 +203,13 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret strin
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) ListClients(searchTerm string, page int, pageSize int) ([]model.OidcClient, utils.PaginationResponse, error) {
|
func (s *OidcService) ListClients(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.OidcClient, utils.PaginationResponse, error) {
|
||||||
var clients []model.OidcClient
|
var clients []model.OidcClient
|
||||||
|
|
||||||
query := s.db.Preload("CreatedBy").Model(&model.OidcClient{})
|
query := s.db.Preload("CreatedBy").Model(&model.OidcClient{})
|
||||||
@@ -154,7 +218,7 @@ func (s *OidcService) ListClients(searchTerm string, page int, pageSize int) ([]
|
|||||||
query = query.Where("name LIKE ?", searchPattern)
|
query = query.Where("name LIKE ?", searchPattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pagination, err := utils.Paginate(page, pageSize, query, &clients)
|
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, utils.PaginationResponse{}, err
|
return nil, utils.PaginationResponse{}, err
|
||||||
}
|
}
|
||||||
@@ -167,6 +231,8 @@ func (s *OidcService) CreateClient(input dto.OidcClientCreateDto, userID string)
|
|||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
CallbackURLs: input.CallbackURLs,
|
CallbackURLs: input.CallbackURLs,
|
||||||
CreatedByID: userID,
|
CreatedByID: userID,
|
||||||
|
IsPublic: input.IsPublic,
|
||||||
|
PkceEnabled: input.IsPublic || input.PkceEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&client).Error; err != nil {
|
if err := s.db.Create(&client).Error; err != nil {
|
||||||
@@ -184,6 +250,8 @@ func (s *OidcService) UpdateClient(clientID string, input dto.OidcClientCreateDt
|
|||||||
|
|
||||||
client.Name = input.Name
|
client.Name = input.Name
|
||||||
client.CallbackURLs = input.CallbackURLs
|
client.CallbackURLs = input.CallbackURLs
|
||||||
|
client.IsPublic = input.IsPublic
|
||||||
|
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||||
|
|
||||||
if err := s.db.Save(&client).Error; err != nil {
|
if err := s.db.Save(&client).Error; err != nil {
|
||||||
return model.OidcClient{}, err
|
return model.OidcClient{}, err
|
||||||
@@ -249,7 +317,7 @@ func (s *OidcService) GetClientLogo(clientID string) (string, string, error) {
|
|||||||
func (s *OidcService) UpdateClientLogo(clientID string, file *multipart.FileHeader) error {
|
func (s *OidcService) UpdateClientLogo(clientID string, file *multipart.FileHeader) error {
|
||||||
fileType := utils.GetFileExtension(file.Filename)
|
fileType := utils.GetFileExtension(file.Filename)
|
||||||
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
|
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
|
||||||
return common.ErrFileTypeNotSupported
|
return &common.FileTypeNotSupportedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, clientID, fileType)
|
imagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, clientID, fileType)
|
||||||
@@ -315,6 +383,7 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
|||||||
|
|
||||||
if strings.Contains(scope, "email") {
|
if strings.Contains(scope, "email") {
|
||||||
claims["email"] = user.Email
|
claims["email"] = user.Email
|
||||||
|
claims["email_verified"] = s.appConfigService.DbConfig.EmailsVerified.Value == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(scope, "groups") {
|
if strings.Contains(scope, "groups") {
|
||||||
@@ -328,14 +397,34 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
|||||||
profileClaims := map[string]interface{}{
|
profileClaims := map[string]interface{}{
|
||||||
"given_name": user.FirstName,
|
"given_name": user.FirstName,
|
||||||
"family_name": user.LastName,
|
"family_name": user.LastName,
|
||||||
"name": user.FirstName + " " + user.LastName,
|
"name": user.FullName(),
|
||||||
"preferred_username": user.Username,
|
"preferred_username": user.Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(scope, "profile") {
|
if strings.Contains(scope, "profile") {
|
||||||
|
// Add profile claims
|
||||||
for k, v := range profileClaims {
|
for k, v := range profileClaims {
|
||||||
claims[k] = v
|
claims[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add custom claims
|
||||||
|
customClaims, err := s.customClaimService.GetCustomClaimsForUserWithUserGroups(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, customClaim := range customClaims {
|
||||||
|
// The value of the custom claim can be a JSON object or a string
|
||||||
|
var jsonValue interface{}
|
||||||
|
json.Unmarshal([]byte(customClaim.Value), &jsonValue)
|
||||||
|
if jsonValue != nil {
|
||||||
|
// It's JSON so we store it as an object
|
||||||
|
claims[customClaim.Key] = jsonValue
|
||||||
|
} else {
|
||||||
|
// Marshalling failed, so we store it as a string
|
||||||
|
claims[customClaim.Key] = customClaim.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.Contains(scope, "email") {
|
if strings.Contains(scope, "email") {
|
||||||
claims["email"] = user.Email
|
claims["email"] = user.Email
|
||||||
@@ -344,19 +433,50 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
|||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) createAuthorizationCode(clientID string, userID string, scope string, nonce string) (string, error) {
|
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) {
|
||||||
randomString, err := utils.GenerateRandomAlphanumericString(32)
|
randomString, err := utils.GenerateRandomAlphanumericString(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
codeChallengeMethodSha256 := strings.ToUpper(codeChallengeMethod) == "S256"
|
||||||
|
|
||||||
oidcAuthorizationCode := model.OidcAuthorizationCode{
|
oidcAuthorizationCode := model.OidcAuthorizationCode{
|
||||||
ExpiresAt: datatype.DateTime(time.Now().Add(15 * time.Minute)),
|
ExpiresAt: datatype.DateTime(time.Now().Add(15 * time.Minute)),
|
||||||
Code: randomString,
|
Code: randomString,
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Scope: scope,
|
Scope: scope,
|
||||||
Nonce: nonce,
|
Nonce: nonce,
|
||||||
|
CodeChallenge: &codeChallenge,
|
||||||
|
CodeChallengeMethodSha256: &codeChallengeMethodSha256,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&oidcAuthorizationCode).Error; err != nil {
|
if err := s.db.Create(&oidcAuthorizationCode).Error; err != nil {
|
||||||
@@ -366,13 +486,41 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc
|
|||||||
return randomString, nil
|
return randomString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCallbackURL(client model.OidcClient, inputCallbackURL string) (callbackURL string, err error) {
|
func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
||||||
|
if codeVerifier == "" || codeChallenge == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !codeChallengeMethodSha256 {
|
||||||
|
return codeVerifier == codeChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute SHA-256 hash of the codeVerifier
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(codeVerifier))
|
||||||
|
codeVerifierHash := h.Sum(nil)
|
||||||
|
|
||||||
|
// Base64 URL encode the verifier hash
|
||||||
|
encodedVerifierHash := base64.RawURLEncoding.EncodeToString(codeVerifierHash)
|
||||||
|
|
||||||
|
return encodedVerifierHash == codeChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OidcService) getCallbackURL(client model.OidcClient, inputCallbackURL string) (callbackURL string, err error) {
|
||||||
if inputCallbackURL == "" {
|
if inputCallbackURL == "" {
|
||||||
return client.CallbackURLs[0], nil
|
return client.CallbackURLs[0], nil
|
||||||
}
|
}
|
||||||
if slices.Contains(client.CallbackURLs, inputCallbackURL) {
|
|
||||||
return inputCallbackURL, nil
|
for _, callbackPattern := range client.CallbackURLs {
|
||||||
|
regexPattern := strings.ReplaceAll(regexp.QuoteMeta(callbackPattern), `\*`, ".*") + "$"
|
||||||
|
matched, err := regexp.MatchString(regexPattern, inputCallbackURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
return inputCallbackURL, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", common.ErrOidcInvalidCallbackURL
|
return "", &common.OidcInvalidCallbackURLError{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fxamacker/cbor/v2"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fxamacker/cbor/v2"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,10 +60,33 @@ func (s *TestService) SeedDatabase() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oneTimeAccessTokens := []model.OneTimeAccessToken{{
|
||||||
|
Base: model.Base{
|
||||||
|
ID: "bf877753-4ea4-4c9c-bbbd-e198bb201cb8",
|
||||||
|
},
|
||||||
|
Token: "HPe6k6uiDRRVuAQV",
|
||||||
|
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||||
|
UserID: users[0].ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Base: model.Base{
|
||||||
|
ID: "d3afae24-fe2d-4a98-abec-cf0b8525096a",
|
||||||
|
},
|
||||||
|
Token: "YCGDtftvsvYWiXd0",
|
||||||
|
ExpiresAt: datatype.DateTime(time.Now().Add(-1 * time.Second)), // expired
|
||||||
|
UserID: users[0].ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, token := range oneTimeAccessTokens {
|
||||||
|
if err := tx.Create(&token).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
userGroups := []model.UserGroup{
|
userGroups := []model.UserGroup{
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "4110f814-56f1-4b28-8998-752b69bc97c0e",
|
ID: "c7ae7c01-28a3-4f3c-9572-1ee734ea8368",
|
||||||
},
|
},
|
||||||
Name: "developers",
|
Name: "developers",
|
||||||
FriendlyName: "Developers",
|
FriendlyName: "Developers",
|
||||||
@@ -99,7 +125,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 {
|
||||||
@@ -138,27 +167,31 @@ func (s *TestService) SeedDatabase() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
publicKey1, err := getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwcOo5KV169KR67QEHrcYkeXE3CCxv2BgwnSq4VYTQxyLtdmKxegexa8JdwFKhKXa2BMI9xaN15BoL6wSCRFJhg==")
|
// To generate a new key pair, run the following command:
|
||||||
publicKey2, err := 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: "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: "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 {
|
||||||
@@ -169,7 +202,7 @@ func (s *TestService) SeedDatabase() error {
|
|||||||
|
|
||||||
webauthnSession := model.WebauthnSession{
|
webauthnSession := model.WebauthnSession{
|
||||||
Challenge: "challenge",
|
Challenge: "challenge",
|
||||||
ExpiresAt: time.Now().Add(1 * time.Hour),
|
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||||
UserVerification: "preferred",
|
UserVerification: "preferred",
|
||||||
}
|
}
|
||||||
if err := tx.Create(&webauthnSession).Error; err != nil {
|
if err := tx.Create(&webauthnSession).Error; err != nil {
|
||||||
@@ -183,21 +216,36 @@ func (s *TestService) SeedDatabase() error {
|
|||||||
func (s *TestService) ResetDatabase() error {
|
func (s *TestService) ResetDatabase() error {
|
||||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
var tables []string
|
var tables []string
|
||||||
if err := tx.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations';").Scan(&tables).Error; err != nil {
|
|
||||||
return err
|
switch common.EnvConfig.DbProvider {
|
||||||
|
case common.DbProviderSqlite:
|
||||||
|
// Query to get all tables for SQLite
|
||||||
|
if err := tx.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations';").Scan(&tables).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case common.DbProviderPostgres:
|
||||||
|
// Query to get all tables for PostgreSQL
|
||||||
|
if err := tx.Raw(`
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public' AND tablename != 'schema_migrations';
|
||||||
|
`).Scan(&tables).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete all rows from all tables
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
if err := tx.Exec("DELETE FROM " + table).Error; err != nil {
|
if err := tx.Exec(fmt.Sprintf("DELETE FROM %s;", table)).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = s.appConfigService.InitDbConfig()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,16 +255,41 @@ func (s *TestService) ResetApplicationImages() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := utils.CopyDirectory("./images", common.EnvConfig.UploadPath+"/application-images"); err != nil {
|
files, err := resources.FS.ReadDir("images")
|
||||||
log.Printf("Error copying directory: %v", err)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
srcFilePath := filepath.Join("images", file.Name())
|
||||||
|
destFilePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", file.Name())
|
||||||
|
|
||||||
|
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TestService) ResetAppConfig() error {
|
||||||
|
// Reseed the config variables
|
||||||
|
if err := s.appConfigService.InitDbConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all app config variables to their default values
|
||||||
|
if err := s.db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&model.AppConfigVariable{}).Update("value", "").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the app config from the database after resetting the values
|
||||||
|
return s.appConfigService.LoadDbConfigFromDb()
|
||||||
|
}
|
||||||
|
|
||||||
// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key
|
// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key
|
||||||
func getCborPublicKey(base64PublicKey string) ([]byte, error) {
|
func (s *TestService) getCborPublicKey(base64PublicKey string) ([]byte, error) {
|
||||||
decodedKey, err := base64.StdEncoding.DecodeString(base64PublicKey)
|
decodedKey, err := base64.StdEncoding.DecodeString(base64PublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode base64 key: %w", err)
|
return nil, fmt.Errorf("failed to decode base64 key: %w", err)
|
||||||
|
|||||||
@@ -2,34 +2,48 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
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, page int, pageSize int) (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) {
|
||||||
query := s.db.Model(&model.UserGroup{})
|
query := s.db.Preload("CustomClaims").Model(&model.UserGroup{})
|
||||||
|
|
||||||
if name != "" {
|
if name != "" {
|
||||||
query = query.Where("name LIKE ?", "%"+name+"%")
|
query = query.Where("name LIKE ?", "%"+name+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err = utils.Paginate(page, pageSize, query, &groups)
|
// As userCount is not a column we need to manually sort it
|
||||||
|
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc"
|
||||||
|
if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection {
|
||||||
|
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
|
||||||
|
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
|
||||||
|
Group("user_groups.id").
|
||||||
|
Order("COUNT(user_groups_users.user_id) " + sortedPaginationRequest.Sort.Direction)
|
||||||
|
|
||||||
|
response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &groups)
|
||||||
|
return groups, response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &groups)
|
||||||
return groups, response, err
|
return groups, response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGroupService) Get(id string) (group model.UserGroup, err error) {
|
func (s *UserGroupService) Get(id string) (group model.UserGroup, err error) {
|
||||||
err = s.db.Where("id = ?", id).Preload("Users").First(&group).Error
|
err = s.db.Where("id = ?", id).Preload("CustomClaims").Preload("Users").First(&group).Error
|
||||||
return group, err
|
return group, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +53,11 @@ func (s *UserGroupService) Delete(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 s.db.Delete(&group).Error
|
return s.db.Delete(&group).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,27 +67,36 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use
|
|||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.LdapID != "" {
|
||||||
|
group.LdapID = &input.LdapID
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.db.Preload("Users").Create(&group).Error; err != nil {
|
if err := s.db.Preload("Users").Create(&group).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
return model.UserGroup{}, common.ErrNameAlreadyInUse
|
return model.UserGroup{}, &common.AlreadyInUseError{Property: "name"}
|
||||||
}
|
}
|
||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
return group, nil
|
return group, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto) (group model.UserGroup, err error) {
|
func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto, allowLdapUpdate bool) (group model.UserGroup, err error) {
|
||||||
group, err = s.Get(id)
|
group, err = s.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
}
|
||||||
|
|
||||||
group.Name = input.Name
|
group.Name = input.Name
|
||||||
group.FriendlyName = input.FriendlyName
|
group.FriendlyName = input.FriendlyName
|
||||||
|
|
||||||
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) {
|
||||||
return model.UserGroup{}, common.ErrNameAlreadyInUse
|
return model.UserGroup{}, &common.AlreadyInUseError{Property: "name"}
|
||||||
}
|
}
|
||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,34 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"log"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"net/url"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
"strings"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
jwtService *JwtService
|
jwtService *JwtService
|
||||||
|
auditLogService *AuditLogService
|
||||||
|
emailService *EmailService
|
||||||
|
appConfigService *AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(db *gorm.DB, jwtService *JwtService) *UserService {
|
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService) *UserService {
|
||||||
return &UserService{db: db, jwtService: jwtService}
|
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService, emailService: emailService, appConfigService: appConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ListUsers(searchTerm string, page int, pageSize int) ([]model.User, utils.PaginationResponse, error) {
|
func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
||||||
var users []model.User
|
var users []model.User
|
||||||
query := s.db.Model(&model.User{})
|
query := s.db.Model(&model.User{})
|
||||||
|
|
||||||
@@ -29,13 +38,13 @@ func (s *UserService) ListUsers(searchTerm string, page int, pageSize int) ([]mo
|
|||||||
query = query.Where("email LIKE ? OR first_name LIKE ? OR username LIKE ?", searchPattern, searchPattern, searchPattern)
|
query = query.Where("email LIKE ? OR first_name LIKE ? OR username LIKE ?", searchPattern, searchPattern, searchPattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pagination, err := utils.Paginate(page, pageSize, query, &users)
|
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users)
|
||||||
return users, pagination, err
|
return users, pagination, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) GetUser(userID string) (model.User, error) {
|
func (s *UserService) GetUser(userID string) (model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
err := s.db.Where("id = ?", userID).First(&user).Error
|
err := s.db.Preload("CustomClaims").Where("id = ?", userID).First(&user).Error
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +54,11 @@ func (s *UserService) DeleteUser(userID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 s.db.Delete(&user).Error
|
return s.db.Delete(&user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +70,10 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
|||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
IsAdmin: input.IsAdmin,
|
IsAdmin: input.IsAdmin,
|
||||||
}
|
}
|
||||||
|
if input.LdapID != "" {
|
||||||
|
user.LdapID = &input.LdapID
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&user).Error; err != nil {
|
if err := s.db.Create(&user).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
return model.User{}, s.checkDuplicatedFields(user)
|
return model.User{}, s.checkDuplicatedFields(user)
|
||||||
@@ -65,11 +83,17 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool) (model.User, error) {
|
func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, allowLdapUpdate bool) (model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||||
return model.User{}, err
|
return model.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
}
|
||||||
|
|
||||||
user.FirstName = updatedUser.FirstName
|
user.FirstName = updatedUser.FirstName
|
||||||
user.LastName = updatedUser.LastName
|
user.LastName = updatedUser.LastName
|
||||||
user.Email = updatedUser.Email
|
user.Email = updatedUser.Email
|
||||||
@@ -88,6 +112,45 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserService) RequestOneTimeAccessEmail(emailAddress, redirectPath string) error {
|
||||||
|
var user model.User
|
||||||
|
if err := s.db.Where("email = ?", emailAddress).First(&user).Error; err != nil {
|
||||||
|
// Do not return error if user not found to prevent email enumeration
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oneTimeAccessToken, err := s.CreateOneTimeAccessToken(user.ID, time.Now().Add(time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf("%s/login/%s", common.EnvConfig.AppURL, oneTimeAccessToken)
|
||||||
|
|
||||||
|
// Add redirect path to the link
|
||||||
|
if strings.HasPrefix(redirectPath, "/") {
|
||||||
|
encodedRedirectPath := url.QueryEscape(redirectPath)
|
||||||
|
link = fmt.Sprintf("%s?redirect=%s", link, encodedRedirectPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := SendEmail(s.emailService, email.Address{
|
||||||
|
Name: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
||||||
|
Link: link,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to send email to '%s': %v\n", user.Email, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Time) (string, error) {
|
func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Time) (string, error) {
|
||||||
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,11 +170,11 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim
|
|||||||
return oneTimeAccessToken.Token, nil
|
return oneTimeAccessToken.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, string, error) {
|
func (s *UserService) ExchangeOneTimeAccessToken(token string, ipAddress, userAgent string) (model.User, string, error) {
|
||||||
var oneTimeAccessToken model.OneTimeAccessToken
|
var oneTimeAccessToken model.OneTimeAccessToken
|
||||||
if err := s.db.Where("token = ? AND expires_at > ?", token, time.Now().Unix()).Preload("User").First(&oneTimeAccessToken).Error; err != nil {
|
if err := s.db.Where("token = ? AND expires_at > ?", token, datatype.DateTime(time.Now())).Preload("User").First(&oneTimeAccessToken).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return model.User{}, "", common.ErrTokenInvalidOrExpired
|
return model.User{}, "", &common.TokenInvalidOrExpiredError{}
|
||||||
}
|
}
|
||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
@@ -124,6 +187,10 @@ func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, stri
|
|||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ipAddress != "" && userAgent != "" {
|
||||||
|
s.auditLogService.Create(model.AuditLogEventOneTimeAccessTokenSignIn, ipAddress, userAgent, oneTimeAccessToken.User.ID, model.AuditLogData{})
|
||||||
|
}
|
||||||
|
|
||||||
return oneTimeAccessToken.User, accessToken, nil
|
return oneTimeAccessToken.User, accessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +200,7 @@ func (s *UserService) SetupInitialAdmin() (model.User, string, error) {
|
|||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
if userCount > 1 {
|
if userCount > 1 {
|
||||||
return model.User{}, "", common.ErrSetupAlreadyCompleted
|
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := model.User{
|
user := model.User{
|
||||||
@@ -149,7 +216,7 @@ func (s *UserService) SetupInitialAdmin() (model.User, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(user.Credentials) > 0 {
|
if len(user.Credentials) > 0 {
|
||||||
return model.User{}, "", common.ErrSetupAlreadyCompleted
|
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.jwtService.GenerateAccessToken(user)
|
token, err := s.jwtService.GenerateAccessToken(user)
|
||||||
@@ -163,11 +230,11 @@ func (s *UserService) SetupInitialAdmin() (model.User, string, error) {
|
|||||||
func (s *UserService) checkDuplicatedFields(user model.User) error {
|
func (s *UserService) checkDuplicatedFields(user model.User) error {
|
||||||
var existingUser model.User
|
var existingUser model.User
|
||||||
if s.db.Where("id != ? AND email = ?", user.ID, user.Email).First(&existingUser).Error == nil {
|
if s.db.Where("id != ? AND email = ?", user.ID, user.Email).First(&existingUser).Error == nil {
|
||||||
return common.ErrEmailTaken
|
return &common.AlreadyInUseError{Property: "email"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.db.Where("id != ? AND username = ?", user.ID, user.Username).First(&existingUser).Error == nil {
|
if s.db.Where("id != ? AND username = ?", user.ID, user.Username).First(&existingUser).Error == nil {
|
||||||
return common.ErrUsernameTaken
|
return &common.AlreadyInUseError{Property: "username"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebAuthnService struct {
|
type WebAuthnService struct {
|
||||||
@@ -22,7 +24,7 @@ type WebAuthnService struct {
|
|||||||
func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, appConfigService *AppConfigService) *WebAuthnService {
|
func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, appConfigService *AppConfigService) *WebAuthnService {
|
||||||
webauthnConfig := &webauthn.Config{
|
webauthnConfig := &webauthn.Config{
|
||||||
RPDisplayName: appConfigService.DbConfig.AppName.Value,
|
RPDisplayName: appConfigService.DbConfig.AppName.Value,
|
||||||
RPID: utils.GetHostFromURL(common.EnvConfig.AppURL),
|
RPID: utils.GetHostnameFromURL(common.EnvConfig.AppURL),
|
||||||
RPOrigins: []string{common.EnvConfig.AppURL},
|
RPOrigins: []string{common.EnvConfig.AppURL},
|
||||||
Timeouts: webauthn.TimeoutsConfig{
|
Timeouts: webauthn.TimeoutsConfig{
|
||||||
Login: webauthn.TimeoutConfig{
|
Login: webauthn.TimeoutConfig{
|
||||||
@@ -55,7 +57,7 @@ func (s *WebAuthnService) BeginRegistration(userID string) (*model.PublicKeyCred
|
|||||||
}
|
}
|
||||||
|
|
||||||
sessionToStore := &model.WebauthnSession{
|
sessionToStore := &model.WebauthnSession{
|
||||||
ExpiresAt: session.Expires,
|
ExpiresAt: datatype.DateTime(session.Expires),
|
||||||
Challenge: session.Challenge,
|
Challenge: session.Challenge,
|
||||||
UserVerification: string(session.UserVerification),
|
UserVerification: string(session.UserVerification),
|
||||||
}
|
}
|
||||||
@@ -79,7 +81,7 @@ func (s *WebAuthnService) VerifyRegistration(sessionID, userID string, r *http.R
|
|||||||
|
|
||||||
session := webauthn.SessionData{
|
session := webauthn.SessionData{
|
||||||
Challenge: storedSession.Challenge,
|
Challenge: storedSession.Challenge,
|
||||||
Expires: storedSession.ExpiresAt,
|
Expires: storedSession.ExpiresAt.ToTime(),
|
||||||
UserID: []byte(userID),
|
UserID: []byte(userID),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ func (s *WebAuthnService) VerifyRegistration(sessionID, userID string, r *http.R
|
|||||||
|
|
||||||
credentialToStore := model.WebauthnCredential{
|
credentialToStore := model.WebauthnCredential{
|
||||||
Name: "New Passkey",
|
Name: "New Passkey",
|
||||||
CredentialID: string(credential.ID),
|
CredentialID: credential.ID,
|
||||||
AttestationType: credential.AttestationType,
|
AttestationType: credential.AttestationType,
|
||||||
PublicKey: credential.PublicKey,
|
PublicKey: credential.PublicKey,
|
||||||
Transport: credential.Transport,
|
Transport: credential.Transport,
|
||||||
@@ -117,7 +119,7 @@ func (s *WebAuthnService) BeginLogin() (*model.PublicKeyCredentialRequestOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
sessionToStore := &model.WebauthnSession{
|
sessionToStore := &model.WebauthnSession{
|
||||||
ExpiresAt: session.Expires,
|
ExpiresAt: datatype.DateTime(session.Expires),
|
||||||
Challenge: session.Challenge,
|
Challenge: session.Challenge,
|
||||||
UserVerification: string(session.UserVerification),
|
UserVerification: string(session.UserVerification),
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,7 @@ func (s *WebAuthnService) BeginLogin() (*model.PublicKeyCredentialRequestOptions
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WebAuthnService) VerifyLogin(sessionID, userID string, credentialAssertionData *protocol.ParsedCredentialAssertionData, ipAddress, userAgent string) (model.User, string, error) {
|
func (s *WebAuthnService) VerifyLogin(sessionID string, credentialAssertionData *protocol.ParsedCredentialAssertionData, ipAddress, userAgent string) (model.User, string, error) {
|
||||||
var storedSession model.WebauthnSession
|
var storedSession model.WebauthnSession
|
||||||
if err := s.db.First(&storedSession, "id = ?", sessionID).Error; err != nil {
|
if err := s.db.First(&storedSession, "id = ?", sessionID).Error; err != nil {
|
||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
@@ -141,7 +143,7 @@ func (s *WebAuthnService) VerifyLogin(sessionID, userID string, credentialAssert
|
|||||||
|
|
||||||
session := webauthn.SessionData{
|
session := webauthn.SessionData{
|
||||||
Challenge: storedSession.Challenge,
|
Challenge: storedSession.Challenge,
|
||||||
Expires: storedSession.ExpiresAt,
|
Expires: storedSession.ExpiresAt.ToTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var user *model.User
|
var user *model.User
|
||||||
@@ -156,16 +158,12 @@ func (s *WebAuthnService) VerifyLogin(sessionID, userID string, credentialAssert
|
|||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Find(&user, "id = ?", userID).Error; err != nil {
|
|
||||||
return model.User{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := s.jwtService.GenerateAccessToken(*user)
|
token, err := s.jwtService.GenerateAccessToken(*user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.auditLogService.CreateNewSignInWithEmail(ipAddress, userAgent, user.ID, model.AuditLogData{})
|
s.auditLogService.CreateNewSignInWithEmail(ipAddress, userAgent, user.ID)
|
||||||
|
|
||||||
return *user, token, nil
|
return *user, token, nil
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
17
backend/internal/utils/cookie/cookie_names.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package cookie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AccessTokenCookieName = "__Host-access_token"
|
||||||
|
var SessionIdCookieName = "__Host-session"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if strings.HasPrefix(common.EnvConfig.AppURL, "http://") {
|
||||||
|
AccessTokenCookieName = "access_token"
|
||||||
|
SessionIdCookieName = "session"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"path"
|
"path"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
)
|
|
||||||
|
|
||||||
const templateComponentsDir = "components"
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
|
)
|
||||||
|
|
||||||
type Template[V any] struct {
|
type Template[V any] struct {
|
||||||
Path string
|
Path string
|
||||||
@@ -35,36 +35,37 @@ type pareseable[V any] interface {
|
|||||||
ParseFS(fs.FS, ...string) (V, error)
|
ParseFS(fs.FS, ...string) (V, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareTemplate[V pareseable[V]](template string, rootTemplate clonable[V], templateDir fs.FS, suffix string) (V, error) {
|
func prepareTemplate[V pareseable[V]](templateFS fs.FS, template string, rootTemplate clonable[V], suffix string) (V, error) {
|
||||||
tmpl, err := rootTemplate.Clone()
|
tmpl, err := rootTemplate.Clone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *new(V), fmt.Errorf("clone root html template: %w", err)
|
return *new(V), fmt.Errorf("clone root template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := fmt.Sprintf("%s%s", template, suffix)
|
filename := fmt.Sprintf("%s%s", template, suffix)
|
||||||
_, err = tmpl.ParseFS(templateDir, filename)
|
templatePath := path.Join("email-templates", filename)
|
||||||
|
_, err = tmpl.ParseFS(templateFS, templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *new(V), fmt.Errorf("parsing html template '%s': %w", template, err)
|
return *new(V), fmt.Errorf("parsing template '%s': %w", template, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmpl, nil
|
return tmpl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrepareTextTemplates(templateDir fs.FS, templates []string) (map[string]*ttemplate.Template, error) {
|
func PrepareTextTemplates(templates []string) (map[string]*ttemplate.Template, error) {
|
||||||
components := path.Join(templateComponentsDir, "*_text.tmpl")
|
components := path.Join("email-templates", "components", "*_text.tmpl")
|
||||||
rootTmpl, err := ttemplate.ParseFS(templateDir, components)
|
rootTmpl, err := ttemplate.ParseFS(resources.FS, components)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to parse templates '%s': %w", components, err)
|
return nil, fmt.Errorf("unable to parse templates '%s': %w", components, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var textTemplates = make(map[string]*ttemplate.Template, len(templates))
|
textTemplates := make(map[string]*ttemplate.Template, len(templates))
|
||||||
for _, tmpl := range templates {
|
for _, tmpl := range templates {
|
||||||
rootTmplClone, err := rootTmpl.Clone()
|
rootTmplClone, err := rootTmpl.Clone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("clone root template: %w", err)
|
return nil, fmt.Errorf("clone root template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
textTemplates[tmpl], err = prepareTemplate[*ttemplate.Template](tmpl, rootTmplClone, templateDir, "_text.tmpl")
|
textTemplates[tmpl], err = prepareTemplate[*ttemplate.Template](resources.FS, tmpl, rootTmplClone, "_text.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse '%s': %w", tmpl, err)
|
return nil, fmt.Errorf("parse '%s': %w", tmpl, err)
|
||||||
}
|
}
|
||||||
@@ -73,21 +74,21 @@ func PrepareTextTemplates(templateDir fs.FS, templates []string) (map[string]*tt
|
|||||||
return textTemplates, nil
|
return textTemplates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrepareHTMLTemplates(templateDir fs.FS, templates []string) (map[string]*htemplate.Template, error) {
|
func PrepareHTMLTemplates(templates []string) (map[string]*htemplate.Template, error) {
|
||||||
components := path.Join(templateComponentsDir, "*_html.tmpl")
|
components := path.Join("email-templates", "components", "*_html.tmpl")
|
||||||
rootTmpl, err := htemplate.ParseFS(templateDir, components)
|
rootTmpl, err := htemplate.ParseFS(resources.FS, components)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to parse templates '%s': %w", components, err)
|
return nil, fmt.Errorf("unable to parse templates '%s': %w", components, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlTemplates = make(map[string]*htemplate.Template, len(templates))
|
htmlTemplates := make(map[string]*htemplate.Template, len(templates))
|
||||||
for _, tmpl := range templates {
|
for _, tmpl := range templates {
|
||||||
rootTmplClone, err := rootTmpl.Clone()
|
rootTmplClone, err := rootTmpl.Clone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("clone root template: %w", err)
|
return nil, fmt.Errorf("clone root template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlTemplates[tmpl], err = prepareTemplate[*htemplate.Template](tmpl, rootTmplClone, templateDir, "_html.tmpl")
|
htmlTemplates[tmpl], err = prepareTemplate[*htemplate.Template](resources.FS, tmpl, rootTmplClone, "_html.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse '%s': %w", tmpl, err)
|
return nil, fmt.Errorf("parse '%s': %w", tmpl, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetFileExtension(filename string) string {
|
func GetFileExtension(filename string) string {
|
||||||
@@ -28,27 +30,8 @@ func GetImageMimeType(ext string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyDirectory(srcDir, destDir string) error {
|
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
|
||||||
files, err := os.ReadDir(srcDir)
|
srcFile, err := resources.FS.Open(srcFilePath)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
srcFilePath := filepath.Join(srcDir, file.Name())
|
|
||||||
destFilePath := filepath.Join(destDir, file.Name())
|
|
||||||
|
|
||||||
err := CopyFile(srcFilePath, destFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyFile(srcFilePath, destFilePath string) error {
|
|
||||||
srcFile, err := os.Open(srcFilePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaginationResponse struct {
|
type PaginationResponse struct {
|
||||||
@@ -11,7 +12,36 @@ type PaginationResponse struct {
|
|||||||
ItemsPerPage int `json:"itemsPerPage"`
|
ItemsPerPage int `json:"itemsPerPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Paginate(page int, pageSize int, db *gorm.DB, result interface{}) (PaginationResponse, error) {
|
type SortedPaginationRequest struct {
|
||||||
|
Pagination struct {
|
||||||
|
Page int `form:"pagination[page]"`
|
||||||
|
Limit int `form:"pagination[limit]"`
|
||||||
|
} `form:"pagination"`
|
||||||
|
Sort struct {
|
||||||
|
Column string `form:"sort[column]"`
|
||||||
|
Direction string `form:"sort[direction]"`
|
||||||
|
} `form:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||||
|
pagination := sortedPaginationRequest.Pagination
|
||||||
|
sort := sortedPaginationRequest.Sort
|
||||||
|
|
||||||
|
capitalizedSortColumn := CapitalizeFirstLetter(sort.Column)
|
||||||
|
|
||||||
|
sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
|
||||||
|
isSortable := sortField.Tag.Get("sortable") == "true"
|
||||||
|
isValidSortOrder := sort.Direction == "asc" || sort.Direction == "desc"
|
||||||
|
|
||||||
|
if sortFieldFound && isSortable && isValidSortOrder {
|
||||||
|
query = query.Order(CamelCaseToSnakeCase(sort.Column) + " " + sort.Direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Paginate(pagination.Page, pagination.Limit, query, result)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
@@ -25,16 +55,21 @@ func Paginate(page int, pageSize int, db *gorm.DB, result interface{}) (Paginati
|
|||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
var totalItems int64
|
var totalItems int64
|
||||||
if err := db.Count(&totalItems).Error; err != nil {
|
if err := query.Count(&totalItems).Error; err != nil {
|
||||||
return PaginationResponse{}, err
|
return PaginationResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
||||||
return PaginationResponse{}, err
|
return PaginationResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
||||||
|
if totalItems == 0 {
|
||||||
|
totalPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
return PaginationResponse{
|
return PaginationResponse{
|
||||||
TotalPages: (totalItems + int64(pageSize) - 1) / int64(pageSize),
|
TotalPages: totalPages,
|
||||||
TotalItems: totalItems,
|
TotalItems: totalItems,
|
||||||
CurrentPage: page,
|
CurrentPage: page,
|
||||||
ItemsPerPage: pageSize,
|
ItemsPerPage: pageSize,
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
|
// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
|
||||||
@@ -29,15 +32,44 @@ func GenerateRandomAlphanumericString(length int) (string, error) {
|
|||||||
return string(result), nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHostFromURL(rawURL string) string {
|
func GetHostnameFromURL(rawURL string) string {
|
||||||
parsedURL, err := url.Parse(rawURL)
|
parsedURL, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return parsedURL.Host
|
return parsedURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringPointer creates a string pointer from a string value
|
// StringPointer creates a string pointer from a string value
|
||||||
func StringPointer(s string) *string {
|
func StringPointer(s string) *string {
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CapitalizeFirstLetter(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
runes := []rune(s)
|
||||||
|
runes[0] = unicode.ToUpper(runes[0])
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CamelCaseToSnakeCase(s string) string {
|
||||||
|
var result []rune
|
||||||
|
for i, r := range s {
|
||||||
|
if unicode.IsUpper(r) && i > 0 {
|
||||||
|
result = append(result, '_')
|
||||||
|
}
|
||||||
|
result = append(result, unicode.ToLower(r))
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CamelCaseToScreamingSnakeCase(s string) string {
|
||||||
|
// Insert underscores before uppercase letters (except the first one)
|
||||||
|
re := regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||||
|
snake := re.ReplaceAllString(s, `${1}_${2}`)
|
||||||
|
|
||||||
|
// Convert to uppercase
|
||||||
|
return strings.ToUpper(snake)
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,5 +76,20 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
.button {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.button-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{{ define "base" }}
|
{{ define "base" }}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="{{ .LogoURL }}" alt="Pocket ID"/>
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
<h1>{{ .AppName }}</h1>
|
<h1>{{ .AppName }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="warning">Warning</div>
|
<div class="warning">Warning</div>
|
||||||
17
backend/resources/email-templates/one-time-access_html.tmpl
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{{ define "base" }}
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
|
<h1>{{ .AppName }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>One-Time Access</h2>
|
||||||
|
<p class="message">
|
||||||
|
Click the button below to sign in to {{ .AppName }} with a one-time access link. This link expires in 15 minutes.
|
||||||
|
</p>
|
||||||
|
<div class="button-container">
|
||||||
|
<a class="button" href="{{ .Data.Link }}" class="button">Sign In</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end -}}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{{ define "base" -}}
|
||||||
|
One-Time Access
|
||||||
|
====================
|
||||||
|
|
||||||
|
Click the link below to sign in to {{ .AppName }} with a one-time access link. This link expires in 15 minutes.
|
||||||
|
|
||||||
|
{{ .Data.Link }}
|
||||||
|
{{ end -}}
|
||||||
11
backend/resources/email-templates/test_html.tmpl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{{ define "base" -}}
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
|
<h1>{{ .AppName }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>This is a test email.</p>
|
||||||
|
</div>
|
||||||
|
{{ end -}}
|
||||||
3
backend/resources/email-templates/test_text.tmpl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{{ define "base" -}}
|
||||||
|
This is a test email.
|
||||||
|
{{ end -}}
|
||||||
8
backend/resources/files.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
// Embedded file systems for the project
|
||||||
|
|
||||||
|
//go:embed email-templates images migrations
|
||||||
|
var FS embed.FS
|
||||||
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 539 B |
1
backend/resources/images/logoDark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015"><path fill="#fff" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
1
backend/resources/images/logoLight.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015"><path fill="#000" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
126
backend/resources/migrations/postgres/20241211111554_init.up.sql
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
CREATE TABLE app_config_variables
|
||||||
|
(
|
||||||
|
key VARCHAR(100) NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL,
|
||||||
|
is_public BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
is_internal BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
default_value TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_groups
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
friendly_name VARCHAR(255) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE users
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
first_name VARCHAR(100),
|
||||||
|
last_name VARCHAR(100),
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE audit_logs
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
event VARCHAR(100) NOT NULL,
|
||||||
|
ip_address INET NOT NULL,
|
||||||
|
data JSONB NOT NULL,
|
||||||
|
user_id UUID REFERENCES users ON DELETE SET NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
country VARCHAR(100),
|
||||||
|
city VARCHAR(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE custom_claims
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
key VARCHAR(255) NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
user_id UUID REFERENCES users ON DELETE CASCADE,
|
||||||
|
user_group_id UUID REFERENCES user_groups ON DELETE CASCADE,
|
||||||
|
CONSTRAINT custom_claims_unique UNIQUE (key, user_id, user_group_id),
|
||||||
|
CHECK (user_id IS NOT NULL OR user_group_id IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE oidc_authorization_codes
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
code VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
scope TEXT NOT NULL,
|
||||||
|
nonce VARCHAR(255),
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||||
|
client_id UUID NOT NULL,
|
||||||
|
code_challenge VARCHAR(255),
|
||||||
|
code_challenge_method_sha256 BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE oidc_clients
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
name VARCHAR(255),
|
||||||
|
secret TEXT,
|
||||||
|
callback_urls JSONB,
|
||||||
|
image_type VARCHAR(10),
|
||||||
|
created_by_id UUID REFERENCES users ON DELETE SET NULL,
|
||||||
|
is_public BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE one_time_access_tokens
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_authorized_oidc_clients
|
||||||
|
(
|
||||||
|
scope VARCHAR(255),
|
||||||
|
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||||
|
client_id UUID NOT NULL REFERENCES oidc_clients ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (user_id, client_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_groups_users
|
||||||
|
(
|
||||||
|
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||||
|
user_group_id UUID NOT NULL REFERENCES user_groups ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (user_id, user_group_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE webauthn_credentials
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
credential_id BYTEA NOT NULL UNIQUE,
|
||||||
|
public_key BYTEA NOT NULL,
|
||||||
|
attestation_type VARCHAR(20) NOT NULL,
|
||||||
|
transport JSONB NOT NULL,
|
||||||
|
user_id UUID REFERENCES users ON DELETE CASCADE,
|
||||||
|
backup_eligible BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
backup_state BOOLEAN DEFAULT FALSE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE webauthn_sessions
|
||||||
|
(
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
challenge VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
user_verification VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oidc_clients DROP COLUMN pkce_enabled;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oidc_clients ADD COLUMN pkce_enabled BOOLEAN DEFAULT FALSE;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN ldap_id;
|
||||||
|
|
||||||
|
ALTER TABLE user_groups
|
||||||
|
DROP COLUMN ldap_id;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN ldap_id TEXT;
|
||||||
|
ALTER TABLE user_groups ADD COLUMN ldap_id TEXT;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id);
|
||||||
|
CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET key = 'emailEnabled' WHERE key = 'emailLoginNotificationEnabled';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET key = 'emailLoginNotificationEnabled' WHERE key = 'emailEnabled';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE users SET ldap_id = '' WHERE ldap_id IS NULL;
|
||||||
|
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
|
||||||