Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
83
CHANGELOG.md
@@ -1,3 +1,86 @@
|
|||||||
|
## [](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)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.19.0...v) (2024-12-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ 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/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
|
||||||
|
|||||||
41
README.md
@@ -10,14 +10,32 @@ The goal of Pocket ID is to be a simple and easy-to-use. There are other self-ho
|
|||||||
|
|
||||||
Additionally, what makes Pocket ID special is that it only supports [passkey](https://www.passkeys.io/) authentication, which means you don’t need a password. Some people might not like this idea at first, but I believe passkeys are the future, and once you try them, you’ll love them. For example, you can now use a physical Yubikey to sign in to all your self-hosted services easily and securely.
|
Additionally, what makes Pocket ID special is that it only supports [passkey](https://www.passkeys.io/) authentication, which means you don’t need a password. Some people might not like this idea at first, but I believe passkeys are the future, and once you try them, you’ll love them. For example, you can now use a physical Yubikey to sign in to all your self-hosted services easily and securely.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [ Pocket ID](#-pocket-id)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Setup](#setup)
|
||||||
|
- [Before you start](#before-you-start)
|
||||||
|
- [Installation with Docker (recommended)](#installation-with-docker-recommended)
|
||||||
|
- [Unraid](#unraid)
|
||||||
|
- [Stand-alone Installation](#stand-alone-installation)
|
||||||
|
- [Nginx Reverse Proxy](#nginx-reverse-proxy)
|
||||||
|
- [Proxy Services with Pocket ID](#proxy-services-with-pocket-id)
|
||||||
|
- [Update](#update)
|
||||||
|
- [Docker](#docker)
|
||||||
|
- [Stand-alone](#stand-alone)
|
||||||
|
- [Environment variables](#environment-variables)
|
||||||
|
- [Account recovery](#account-recovery)
|
||||||
|
- [Contribute](#contribute)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue.
|
> Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue.
|
||||||
|
|
||||||
### Before you start
|
### Before you start
|
||||||
|
|
||||||
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.
|
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).
|
||||||
|
|
||||||
### Installation with Docker (recommended)
|
### Installation with Docker (recommended)
|
||||||
|
|
||||||
@@ -78,14 +96,14 @@ Required tools:
|
|||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
# Optional: Start Caddy (You can use any other reverse proxy)
|
||||||
cd ..
|
cd ..
|
||||||
pm2 start --name pocket-id-caddy caddy -- run --config Caddyfile
|
pm2 start --name pocket-id-caddy caddy -- run --config reverse-proxy/Caddyfile
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now sign in with the admin account on `http://localhost/login/setup`.
|
You can now sign in with the admin account on `http://localhost/login/setup`.
|
||||||
|
|
||||||
### Nginx Reverse Proxy
|
### Nginx Reverse Proxy
|
||||||
|
|
||||||
To use Nginx in front of Pocket ID, add the following configuration to increase the header buffer size because, as SvelteKit generates larger headers.
|
To use Nginx as a reverse proxy for Pocket ID, update the configuration to increase the header buffer size. This adjustment is necessary because SvelteKit generates larger headers, which may exceed the default buffer limits.
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
proxy_busy_buffers_size 512k;
|
proxy_busy_buffers_size 512k;
|
||||||
@@ -95,7 +113,7 @@ proxy_buffer_size 256k;
|
|||||||
|
|
||||||
## Proxy Services with Pocket ID
|
## 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/oauth2-proxy) to add authentication to your services that don't support OIDC.
|
The goal of Pocket ID is to function exclusively as an OIDC provider. As such, we don't have a built-in proxy provider. However, you can use other tools that act as a middleware to protect your services and support OIDC as an authentication provider.
|
||||||
|
|
||||||
See the [guide](docs/proxy-services.md) for more information.
|
See the [guide](docs/proxy-services.md) for more information.
|
||||||
|
|
||||||
@@ -136,7 +154,7 @@ docker compose up -d
|
|||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
# Optional: Start Caddy (You can use any other reverse proxy)
|
||||||
cd ..
|
cd ..
|
||||||
pm2 start caddy --name pocket-id-caddy -- run --config Caddyfile
|
pm2 start caddy --name pocket-id-caddy -- run --config reverse-proxy/Caddyfile
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
@@ -157,6 +175,17 @@ docker compose up -d
|
|||||||
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
||||||
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
||||||
|
|
||||||
|
|
||||||
|
## Account recovery
|
||||||
|
|
||||||
|
There are two ways to create a one-time access link for a user:
|
||||||
|
|
||||||
|
1. **UI**: An admin can create a one-time access link for the user in the admin panel under the "Users" tab by clicking on the three dots next to the user's name and selecting "One-time link".
|
||||||
|
2. **Terminal**: You can create a one-time access link for a user by running the `scripts/create-one-time-access-token.sh` script. To execute this script with Docker you have to run the following command:
|
||||||
|
```bash
|
||||||
|
docker compose exec pocket-id sh "sh scripts/create-one-time-access-token.sh <username or email>"
|
||||||
|
```
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
You're very welcome to contribute to Pocket ID! Please follow the [contribution guide](/CONTRIBUTING.md) to get started.
|
You're very welcome to contribute to Pocket ID! Please follow the [contribution guide](/CONTRIBUTING.md) to get started.
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ SQLITE_DB_PATH=data/pocket-id.db
|
|||||||
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
||||||
UPLOAD_PATH=data/uploads
|
UPLOAD_PATH=data/uploads
|
||||||
PORT=8080
|
PORT=8080
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ require (
|
|||||||
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.1
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.31.0
|
||||||
golang.org/x/time v0.6.0
|
golang.org/x/time v0.6.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.6
|
||||||
@@ -23,12 +23,15 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/bytedance/sonic v1.12.3 // indirect
|
github.com/bytedance/sonic v1.12.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-webauthn/x v0.1.14 // indirect
|
github.com/go-webauthn/x v0.1.14 // indirect
|
||||||
@@ -61,10 +64,10 @@ require (
|
|||||||
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.10.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
||||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
@@ -37,8 +40,12 @@ 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-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-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||||
|
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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
@@ -70,11 +77,15 @@ github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u
|
|||||||
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/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
@@ -83,6 +94,12 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
|||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
|
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=
|
||||||
@@ -146,6 +163,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
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/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=
|
||||||
@@ -158,6 +176,7 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
|||||||
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 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
@@ -172,27 +191,98 @@ 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.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
|
||||||
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||||
|
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.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.6.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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
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=
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package bootstrap
|
|||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
|
"github.com/stonith404/pocket-id/backend/resources"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ import (
|
|||||||
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 +29,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,7 +2,6 @@ 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/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +10,5 @@ func Bootstrap() {
|
|||||||
appConfigService := service.NewAppConfigService(db)
|
appConfigService := service.NewAppConfigService(db)
|
||||||
|
|
||||||
initApplicationImages()
|
initApplicationImages()
|
||||||
job.RegisterJobs(db)
|
|
||||||
initRouter(db, appConfigService)
|
initRouter(db, appConfigService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"github.com/golang-migrate/migrate/v4/database"
|
"github.com/golang-migrate/migrate/v4/database"
|
||||||
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
|
"github.com/stonith404/pocket-id/backend/resources"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -42,20 +44,31 @@ func newDatabase() (db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run migrations
|
// Run migrations
|
||||||
m, err := migrate.NewWithDatabaseInstance(
|
if err := migrateDatabase(driver); err != nil {
|
||||||
"file://migrations/"+string(common.EnvConfig.DbProvider),
|
log.Fatalf("failed to run migrations: %v", err)
|
||||||
"pocket-id", driver,
|
}
|
||||||
)
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDatabase(driver database.Driver) error {
|
||||||
|
// Use the embedded migrations
|
||||||
|
source, err := iofs.New(resources.FS, "migrations/"+string(common.EnvConfig.DbProvider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create migration instance: %v", err)
|
return fmt.Errorf("failed to create embedded migration source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration instance: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.Up()
|
err = m.Up()
|
||||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
log.Fatalf("failed to apply migrations: %v", err)
|
return fmt.Errorf("failed to apply migrations: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return db
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectDatabase() (db *gorm.DB, err error) {
|
func connectDatabase() (db *gorm.DB, err error) {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ 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/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/controller"
|
"github.com/stonith404/pocket-id/backend/internal/controller"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/job"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
@@ -29,8 +29,7 @@ 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, db, 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)
|
||||||
}
|
}
|
||||||
@@ -39,27 +38,34 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) {
|
|||||||
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
||||||
jwtService := service.NewJwtService(appConfigService)
|
jwtService := service.NewJwtService(appConfigService)
|
||||||
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
||||||
userService := service.NewUserService(db, jwtService, auditLogService)
|
userService := service.NewUserService(db, jwtService, auditLogService, emailService)
|
||||||
customClaimService := service.NewCustomClaimService(db)
|
customClaimService := service.NewCustomClaimService(db)
|
||||||
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
||||||
testService := service.NewTestService(db, appConfigService)
|
testService := service.NewTestService(db, appConfigService)
|
||||||
userGroupService := service.NewUserGroupService(db)
|
userGroupService := service.NewUserGroupService(db)
|
||||||
|
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.NewErrorHandlerMiddleware().Add())
|
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||||
r.Use(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60))
|
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, appConfigService)
|
controller.NewUserController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService)
|
||||||
controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService, emailService)
|
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)
|
controller.NewCustomClaimController(apiGroup, jwtAuthMiddleware, customClaimService)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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
|
type DbProvider string
|
||||||
@@ -22,7 +23,6 @@ type EnvConfigSchema struct {
|
|||||||
UploadPath string `env:"UPLOAD_PATH"`
|
UploadPath string `env:"UPLOAD_PATH"`
|
||||||
Port string `env:"BACKEND_PORT"`
|
Port string `env:"BACKEND_PORT"`
|
||||||
Host string `env:"HOST"`
|
Host string `env:"HOST"`
|
||||||
EmailTemplatesPath string `env:"EMAIL_TEMPLATES_PATH"`
|
|
||||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,6 @@ var EnvConfig = &EnvConfigSchema{
|
|||||||
AppURL: "http://localhost",
|
AppURL: "http://localhost",
|
||||||
Port: "8080",
|
Port: "8080",
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
EmailTemplatesPath: "./email-templates",
|
|
||||||
MaxMindLicenseKey: "",
|
MaxMindLicenseKey: "",
|
||||||
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
|
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return 400 }
|
|||||||
|
|
||||||
type OidcInvalidCallbackURLError struct{}
|
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) Error() string {
|
||||||
|
return "invalid callback URL, it might be necessary for an admin to fix this"
|
||||||
|
}
|
||||||
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 }
|
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
type FileTypeNotSupportedError struct{}
|
type FileTypeNotSupportedError struct{}
|
||||||
@@ -95,7 +97,7 @@ func (e *MissingPermissionError) HttpStatusCode() int { return http.StatusForbid
|
|||||||
type TooManyRequestsError struct{}
|
type TooManyRequestsError struct{}
|
||||||
|
|
||||||
func (e *TooManyRequestsError) Error() string {
|
func (e *TooManyRequestsError) Error() string {
|
||||||
return "Too many requests. Please wait a while before trying again."
|
return "Too many requests"
|
||||||
}
|
}
|
||||||
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||||
|
|
||||||
@@ -160,3 +162,17 @@ func (e *OidcMissingCodeChallengeError) Error() string {
|
|||||||
return "Missing code challenge"
|
return "Missing code challenge"
|
||||||
}
|
}
|
||||||
func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
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 }
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ func NewAppConfigController(
|
|||||||
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
|
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
|
||||||
appConfigService *service.AppConfigService,
|
appConfigService *service.AppConfigService,
|
||||||
emailService *service.EmailService,
|
emailService *service.EmailService,
|
||||||
|
ldapService *service.LdapService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
acc := &AppConfigController{
|
acc := &AppConfigController{
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
emailService: emailService,
|
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)
|
||||||
@@ -34,11 +36,13 @@ func NewAppConfigController(
|
|||||||
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/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
|
emailService *service.EmailService
|
||||||
|
ldapService *service.LdapService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||||
@@ -182,8 +186,19 @@ func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, ol
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
|
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
|
||||||
err := acc.emailService.SendTestEmail()
|
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 {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
@@ -23,12 +23,16 @@ 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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,11 +153,14 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/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"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) {
|
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,
|
appConfigService: appConfigService,
|
||||||
}
|
}
|
||||||
|
|
||||||
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
|
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
|
||||||
@@ -29,19 +30,23 @@ 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
|
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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -60,7 +65,7 @@ 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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -76,7 +81,7 @@ 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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -92,7 +97,7 @@ 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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -107,7 +112,7 @@ func (uc *UserController) createUserHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := uc.UserService.CreateUser(input)
|
user, err := uc.userService.CreateUser(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -127,7 +132,7 @@ 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" {
|
if uc.appConfigService.DbConfig.AllowOwnAccountEdit.Value != "true" {
|
||||||
c.Error(&common.AccountEditNotAllowedError{})
|
c.Error(&common.AccountEditNotAllowedError{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -141,7 +146,7 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := uc.UserService.CreateOneTimeAccessToken(input.UserID, input.ExpiresAt, c.ClientIP(), c.Request.UserAgent())
|
token, err := uc.userService.CreateOneTimeAccessToken(input.UserID, input.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -150,8 +155,24 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, gin.H{"token": token})
|
c.JSON(http.StatusCreated, gin.H{"token": token})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
|
||||||
|
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 {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
||||||
user, token, err := uc.UserService.ExchangeOneTimeAccessToken(c.Param("token"))
|
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Param("token"), c.ClientIP(), c.Request.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -163,12 +184,12 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, 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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -180,7 +201,7 @@ func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +219,7 @@ 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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) {
|
func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) {
|
||||||
@@ -28,16 +27,20 @@ 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
|
||||||
|
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
groups, pagination, err := ugc.UserGroupService.List(searchTerm, page, pageSize)
|
groups, pagination, err := ugc.UserGroupService.List(searchTerm, sortedPaginationRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
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
|
||||||
@@ -104,7 +107,7 @@ func (ugc *UserGroupController) update(c *gin.Context) {
|
|||||||
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 {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,8 +14,8 @@ import (
|
|||||||
"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)
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ 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) {
|
||||||
@@ -40,7 +42,7 @@ func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", false, true)
|
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
|
||||||
c.JSON(http.StatusOK, options.Response)
|
c.JSON(http.StatusOK, options.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ func (wc *WebauthnController) beginLoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", false, true)
|
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
|
||||||
c.JSON(http.StatusOK, options.Response)
|
c.JSON(http.StatusOK, options.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
utils.AddAccessTokenCookie(c, wc.appConfigService.DbConfig.SessionDuration.Value, token)
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +165,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)
|
utils.AddAccessTokenCookie(c, "0", "")
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,31 @@ 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"`
|
||||||
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||||
EmailEnabled string `json:"emailEnabled" binding:"required"`
|
SmtHost string `json:"smtpHost"`
|
||||||
SmtHost string `json:"smtpHost"`
|
SmtpPort string `json:"smtpPort"`
|
||||||
SmtpPort string `json:"smtpPort"`
|
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
SmtpUser string `json:"smtpUser"`
|
||||||
SmtpUser string `json:"smtpUser"`
|
SmtpPassword string `json:"smtpPassword"`
|
||||||
SmtpPassword string `json:"smtpPassword"`
|
SmtpTls string `json:"smtpTls"`
|
||||||
SmtpTls string `json:"smtpTls"`
|
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||||
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"`
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type OidcClientDto struct {
|
|||||||
PublicOidcClientDto
|
PublicOidcClientDto
|
||||||
CallbackURLs []string `json:"callbackURLs"`
|
CallbackURLs []string `json:"callbackURLs"`
|
||||||
IsPublic bool `json:"isPublic"`
|
IsPublic bool `json:"isPublic"`
|
||||||
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
CreatedBy UserDto `json:"createdBy"`
|
CreatedBy UserDto `json:"createdBy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ 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,urlList"`
|
||||||
IsPublic bool `json:"isPublic"`
|
IsPublic bool `json:"isPublic"`
|
||||||
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeOidcClientRequestDto struct {
|
type AuthorizeOidcClientRequestDto struct {
|
||||||
|
|||||||
@@ -10,17 +10,24 @@ type UserDto struct {
|
|||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type UserGroupDtoWithUsers struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
Users []UserDto `json:"users"`
|
Users []UserDto `json:"users"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,12 +20,14 @@ type UserGroupDtoWithUserCount struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
UserCount int64 `json:"userCount"`
|
UserCount int64 `json:"userCount"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
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 {
|
||||||
|
|||||||
@@ -28,13 +28,6 @@ var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
|||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
var validateUserGroupName validator.Func = func(fl validator.FieldLevel) bool {
|
|
||||||
// The string can only contain lowercase letters, numbers, and underscores
|
|
||||||
regex := "^[a-z0-9_]*$"
|
|
||||||
matched, _ := regexp.MatchString(regex, fl.Field().String())
|
|
||||||
return matched
|
|
||||||
}
|
|
||||||
|
|
||||||
var validateClaimKey validator.Func = func(fl validator.FieldLevel) bool {
|
var validateClaimKey validator.Func = func(fl validator.FieldLevel) bool {
|
||||||
// The string can only contain letters and numbers
|
// The string can only contain letters and numbers
|
||||||
regex := "^[A-Za-z0-9]*$"
|
regex := "^[A-Za-z0-9]*$"
|
||||||
@@ -53,13 +46,6 @@ func init() {
|
|||||||
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 err := v.RegisterValidation("userGroupName", validateUserGroupName); 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("claimKey", validateClaimKey); 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)
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import (
|
|||||||
"github.com/go-co-op/gocron/v2"
|
"github.com/go-co-op/gocron/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
|
datatype "github.com/stonith404/pocket-id/backend/internal/model/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 +30,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/stonith404/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
|
||||||
|
}
|
||||||
@@ -16,8 +16,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()
|
||||||
@@ -29,7 +33,7 @@ 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() {
|
||||||
c.Error(&common.TooManyRequestsError{})
|
c.Error(&common.TooManyRequestsError{})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
@@ -45,12 +49,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()
|
||||||
@@ -64,7 +64,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()
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ type AppConfigVariable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
|
// General
|
||||||
AppName AppConfigVariable
|
AppName AppConfigVariable
|
||||||
SessionDuration AppConfigVariable
|
SessionDuration AppConfigVariable
|
||||||
EmailsVerified AppConfigVariable
|
EmailsVerified AppConfigVariable
|
||||||
AllowOwnAccountEdit AppConfigVariable
|
AllowOwnAccountEdit AppConfigVariable
|
||||||
|
// Internal
|
||||||
BackgroundImageType AppConfigVariable
|
BackgroundImageType AppConfigVariable
|
||||||
LogoLightImageType AppConfigVariable
|
LogoLightImageType AppConfigVariable
|
||||||
LogoDarkImageType AppConfigVariable
|
LogoDarkImageType AppConfigVariable
|
||||||
|
// Email
|
||||||
EmailEnabled AppConfigVariable
|
|
||||||
SmtpHost AppConfigVariable
|
SmtpHost AppConfigVariable
|
||||||
SmtpPort AppConfigVariable
|
SmtpPort AppConfigVariable
|
||||||
SmtpFrom AppConfigVariable
|
SmtpFrom AppConfigVariable
|
||||||
@@ -27,4 +27,21 @@ type AppConfig struct {
|
|||||||
SmtpPassword AppConfigVariable
|
SmtpPassword AppConfigVariable
|
||||||
SmtpTls AppConfigVariable
|
SmtpTls AppConfigVariable
|
||||||
SmtpSkipCertVerify AppConfigVariable
|
SmtpSkipCertVerify AppConfigVariable
|
||||||
|
EmailLoginNotificationEnabled AppConfigVariable
|
||||||
|
EmailOneTimeAccessEnabled AppConfigVariable
|
||||||
|
// LDAP
|
||||||
|
LdapEnabled AppConfigVariable
|
||||||
|
LdapUrl AppConfigVariable
|
||||||
|
LdapBindDn AppConfigVariable
|
||||||
|
LdapBindPassword AppConfigVariable
|
||||||
|
LdapBase 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
// 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) {
|
||||||
|
|||||||
@@ -36,12 +36,13 @@ 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
|
IsPublic bool
|
||||||
|
PkceEnabled bool
|
||||||
|
|
||||||
CreatedByID string
|
CreatedByID string
|
||||||
CreatedBy User
|
CreatedBy User
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import (
|
|||||||
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
|
||||||
|
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
|
|||||||
@@ -2,8 +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
|
CustomClaims []CustomClaim
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppConfigService struct {
|
type AppConfigService struct {
|
||||||
@@ -30,6 +31,7 @@ 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",
|
||||||
@@ -52,6 +54,7 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
IsPublic: true,
|
IsPublic: true,
|
||||||
DefaultValue: "true",
|
DefaultValue: "true",
|
||||||
},
|
},
|
||||||
|
// Internal
|
||||||
BackgroundImageType: model.AppConfigVariable{
|
BackgroundImageType: model.AppConfigVariable{
|
||||||
Key: "backgroundImageType",
|
Key: "backgroundImageType",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -70,11 +73,7 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
DefaultValue: "svg",
|
DefaultValue: "svg",
|
||||||
},
|
},
|
||||||
EmailEnabled: model.AppConfigVariable{
|
// Email
|
||||||
Key: "emailEnabled",
|
|
||||||
Type: "bool",
|
|
||||||
DefaultValue: "false",
|
|
||||||
},
|
|
||||||
SmtpHost: model.AppConfigVariable{
|
SmtpHost: model.AppConfigVariable{
|
||||||
Key: "smtpHost",
|
Key: "smtpHost",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -105,6 +104,76 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
Type: "bool",
|
Type: "bool",
|
||||||
DefaultValue: "false",
|
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",
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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) {
|
||||||
@@ -119,6 +188,13 @@ func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]mode
|
|||||||
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()
|
||||||
|
|||||||
@@ -58,8 +58,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)
|
||||||
@@ -84,11 +84,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,25 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
htemplate "html/template"
|
htemplate "html/template"
|
||||||
"io/fs"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
"mime/quotedprintable"
|
||||||
"net"
|
"net"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var netDialer = &net.Dialer{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
type EmailService struct {
|
type EmailService struct {
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
@@ -26,13 +29,13 @@ type EmailService struct {
|
|||||||
textTemplates map[string]*ttemplate.Template
|
textTemplates map[string]*ttemplate.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmailService(appConfigService *AppConfigService, db *gorm.DB, 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)
|
||||||
}
|
}
|
||||||
@@ -45,9 +48,9 @@ func NewEmailService(appConfigService *AppConfigService, db *gorm.DB, templateDi
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) SendTestEmail() error {
|
func (srv *EmailService) SendTestEmail(recipientUserId string) error {
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := srv.db.First(&user).Error; err != nil {
|
if err := srv.db.First(&user, "id = ?", recipientUserId).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +62,6 @@ func (srv *EmailService) SendTestEmail() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
||||||
// Check if SMTP settings are set
|
|
||||||
if srv.appConfigService.DbConfig.EmailEnabled.Value != "true" {
|
|
||||||
return errors.New("email not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
||||||
@@ -113,11 +111,13 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
tlsConfig,
|
tlsConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
defer client.Quit()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
||||||
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
||||||
|
|
||||||
@@ -142,7 +142,11 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
||||||
conn, err := tls.Dial("tcp", serverAddr, tlsConfig)
|
tlsDialer := &tls.Dialer{
|
||||||
|
NetDialer: netDialer,
|
||||||
|
Config: tlsConfig,
|
||||||
|
}
|
||||||
|
conn, err := tlsDialer.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
@@ -157,7 +161,7 @@ func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
||||||
conn, err := net.Dial("tcp", serverAddr)
|
conn, err := netDialer.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
/**
|
/**
|
||||||
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 +27,13 @@ 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{}]{
|
var TestTemplate = email.Template[struct{}]{
|
||||||
Path: "test",
|
Path: "test",
|
||||||
Title: func(data *email.TemplateData[struct{}]) string {
|
Title: func(data *email.TemplateData[struct{}]) string {
|
||||||
@@ -42,5 +49,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, TestTemplate.Path}
|
var emailTemplatesPaths = []string{NewLoginTemplate.Path, OneTimeAccessTemplate.Path, TestTemplate.Path}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
@@ -96,7 +95,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 +124,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/stonith404/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/stonith404/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 := "(objectClass=groupOfUniqueNames)"
|
||||||
|
|
||||||
|
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).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 := "(objectClass=person)"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -131,8 +131,8 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret, code
|
|||||||
return "", "", &common.OidcInvalidAuthorizationCodeError{}
|
return "", "", &common.OidcInvalidAuthorizationCodeError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the client is public, the code verifier must match the code challenge
|
// If the client is public or PKCE is enabled, the code verifier must match the code challenge
|
||||||
if client.IsPublic {
|
if client.IsPublic || client.PkceEnabled {
|
||||||
if !s.validateCodeVerifier(codeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
if !s.validateCodeVerifier(codeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
||||||
return "", "", &common.OidcInvalidCodeVerifierError{}
|
return "", "", &common.OidcInvalidCodeVerifierError{}
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ func (s *OidcService) GetClient(clientID string) (model.OidcClient, error) {
|
|||||||
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{})
|
||||||
@@ -176,7 +176,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
|
||||||
}
|
}
|
||||||
@@ -189,6 +189,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 {
|
||||||
@@ -207,6 +209,7 @@ 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.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
|
||||||
@@ -406,6 +409,10 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
func (s *OidcService) validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
||||||
|
if codeVerifier == "" || codeChallenge == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if !codeChallengeMethodSha256 {
|
if !codeChallengeMethodSha256 {
|
||||||
return codeVerifier == codeChallenge
|
return codeVerifier == codeChallenge
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/stonith404/pocket-id/backend/resources"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
@@ -57,6 +59,29 @@ 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{
|
||||||
@@ -222,11 +247,21 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,26 @@ func NewUserGroupService(db *gorm.DB) *UserGroupService {
|
|||||||
return &UserGroupService{db: db}
|
return &UserGroupService{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Preload("CustomClaims").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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +51,10 @@ func (s *UserGroupService) Delete(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if group.LdapID != nil {
|
||||||
|
return &common.LdapUserGroupUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Delete(&group).Error
|
return s.db.Delete(&group).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +62,7 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use
|
|||||||
group = model.UserGroup{
|
group = model.UserGroup{
|
||||||
FriendlyName: input.FriendlyName,
|
FriendlyName: input.FriendlyName,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
|
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 {
|
||||||
@@ -57,14 +74,19 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if group.LdapID != nil && !allowLdapUpdate {
|
||||||
|
return model.UserGroup{}, &common.LdapUserGroupUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
group.Name = input.Name
|
group.Name = input.Name
|
||||||
group.FriendlyName = input.FriendlyName
|
group.FriendlyName = input.FriendlyName
|
||||||
|
group.LdapID = &input.LdapID
|
||||||
|
|
||||||
if err := s.db.Preload("Users").Save(&group).Error; err != nil {
|
if err := s.db.Preload("Users").Save(&group).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,13 +20,14 @@ type UserService struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
jwtService *JwtService
|
jwtService *JwtService
|
||||||
auditLogService *AuditLogService
|
auditLogService *AuditLogService
|
||||||
|
emailService *EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService) *UserService {
|
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService) *UserService {
|
||||||
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService}
|
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService, emailService: emailService}
|
||||||
}
|
}
|
||||||
|
|
||||||
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{})
|
||||||
|
|
||||||
@@ -30,7 +36,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +52,10 @@ func (s *UserService) DeleteUser(userID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.LdapID != nil {
|
||||||
|
return &common.LdapUserUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Delete(&user).Error
|
return s.db.Delete(&user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +66,7 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
|||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
IsAdmin: input.IsAdmin,
|
IsAdmin: input.IsAdmin,
|
||||||
|
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) {
|
||||||
@@ -66,11 +77,16 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.LdapID != nil && !allowLdapUpdate {
|
||||||
|
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
|
||||||
@@ -89,7 +105,46 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Time, ipAddress, userAgent string) (string, error) {
|
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) {
|
||||||
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -105,14 +160,12 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.auditLogService.Create(model.AuditLogEventOneTimeAccessTokenSignIn, ipAddress, userAgent, userID, model.AuditLogData{})
|
|
||||||
|
|
||||||
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.TokenInvalidOrExpiredError{}
|
return model.User{}, "", &common.TokenInvalidOrExpiredError{}
|
||||||
}
|
}
|
||||||
@@ -127,6 +180,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,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{
|
||||||
|
|||||||
12
backend/internal/utils/cookie_util.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddAccessTokenCookie(c *gin.Context, sessionDurationInMinutes string, token string) {
|
||||||
|
sessionDurationInMinutesParsed, _ := strconv.Atoi(sessionDurationInMinutes)
|
||||||
|
maxAge := sessionDurationInMinutesParsed * 60
|
||||||
|
c.SetCookie("access_token", token, maxAge, "/", "", true, true)
|
||||||
|
}
|
||||||
@@ -2,14 +2,13 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/stonith404/pocket-id/backend/resources"
|
||||||
htemplate "html/template"
|
htemplate "html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"path"
|
"path"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const templateComponentsDir = "components"
|
|
||||||
|
|
||||||
type Template[V any] struct {
|
type Template[V any] struct {
|
||||||
Path string
|
Path string
|
||||||
Title func(data *TemplateData[V]) string
|
Title func(data *TemplateData[V]) string
|
||||||
@@ -35,36 +34,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 +73,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/stonith404/pocket-id/backend/resources"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
@@ -28,27 +29,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,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
|
// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
|
||||||
@@ -29,15 +30,35 @@ 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 -}}
|
||||||
@@ -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>
|
</div>
|
||||||
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 |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 434 B |
@@ -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 @@
|
|||||||
|
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,2 @@
|
|||||||
|
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';
|
||||||