Compare commits

...

137 commits
v1.3.1 ... oss

Author SHA1 Message Date
07b517c605
Update Pangolin to v1.5.1 2025-06-12 10:40:46 +02:00
Owen Schwartz
ba33064852
Merge pull request #885 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-45f2eb23f4
Bump @dotenvx/dotenvx from 1.44.1 to 1.44.2 in the dev-patch-updates group
2025-06-10 22:29:13 -04:00
dependabot[bot]
94b5aadd76
Bump @dotenvx/dotenvx in the dev-patch-updates group
Bumps the dev-patch-updates group with 1 update: [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx).


Updates `@dotenvx/dotenvx` from 1.44.1 to 1.44.2
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.44.1...v1.44.2)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.44.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 01:41:04 +00:00
Owen Schwartz
6acc2b6a17
Merge pull request #871 from Lokowitz/feat-add-test
feat - add test action
2025-06-10 12:38:32 -04:00
Marvin
83b4976305
Update Dockerfile 2025-06-10 15:59:58 +02:00
Marvin
b1cbb1b50f
Update Dockerfile.pg 2025-06-10 15:59:30 +02:00
Marvin
ff9e5a383b
Update test.yml 2025-06-10 14:00:34 +02:00
Marvin
fc6c93a08a
Update Dockerfile.pg 2025-06-10 08:49:56 +02:00
Marvin
9897c53ed3
Update Dockerfile 2025-06-10 08:49:48 +02:00
Marvin
ced34dd2c6
Update Dockerfile.pg 2025-06-10 08:47:25 +02:00
Marvin
92caac309a
Update Dockerfile 2025-06-10 08:43:16 +02:00
Marvin
3c7a91a047
Update test.yml 2025-06-10 08:42:09 +02:00
Marvin
571db825ad
Merge branch 'fosrl:main' into feat-add-test 2025-06-10 08:06:48 +02:00
Owen Schwartz
5f9c9eed0a
Merge pull request #880 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-ec219de246
Bump @types/react from 19.1.6 to 19.1.7 in the dev-patch-updates group
2025-06-09 22:03:06 -04:00
dependabot[bot]
97ef363461
Bump @types/react from 19.1.6 to 19.1.7 in the dev-patch-updates group
Bumps the dev-patch-updates group with 1 update: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@types/react` from 19.1.6 to 19.1.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 01:20:20 +00:00
Milo Schwartz
6b4e52a725
Merge pull request #877 from fosrl/dev
1.5.1
2025-06-09 17:40:35 -04:00
miloschwartz
e41eafd497
enhance link styling and bump traefik version 2025-06-09 17:38:18 -04:00
Owen Schwartz
d70396a664
Merge pull request #876 from thijsvanloef/fix/define-files-in-eslint
Add Typescript specific linting & define files in eslint
2025-06-09 17:15:23 -04:00
Thijs van Loef
3d59556bcd
readd accidentally removed line 2025-06-09 22:38:38 +02:00
Thijs van Loef
c7018e92b0
Merge branch 'dev' into fix/define-files-in-eslint 2025-06-09 22:36:56 +02:00
Thijs van Loef
a575bace39 add typescript eslint 2025-06-09 22:32:38 +02:00
Thijs van Loef
2047aa30e1 add typescript specific linting 2025-06-09 22:25:27 +02:00
Thijs van Loef
3b10453af3 define files in eslint 2025-06-09 22:18:38 +02:00
Owen
3257edc2a0
Add link to docs for newt 2025-06-09 14:23:53 -04:00
miloschwartz
cd54e7dd38
pick first port on select container 2025-06-09 13:01:53 -04:00
Owen
9177eaba22
Merge branch 'main' into dev 2025-06-09 11:22:35 -04:00
Owen Schwartz
33ae2e08cc
Merge pull request #872 from Lokowitz/fix-dependabot
fix - dependabot
2025-06-09 11:17:45 -04:00
Owen Schwartz
4fc61386d3
Merge pull request #867 from thijsvanloef/fix-767/unit-aware-sorting
Fix - Sort Data In/Out correctly in Sites Management page in the Pangolin Dashboard
2025-06-09 11:17:15 -04:00
Owen
c409266954
Fix #860 2025-06-09 11:13:58 -04:00
Owen
57315a36ee
Merge branch 'main' into dev 2025-06-09 11:07:42 -04:00
Owen Schwartz
63637b91a8
Merge pull request #874 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-637b1bbe0a
Bump the dev-patch-updates group across 1 directory with 4 updates
2025-06-09 11:06:32 -04:00
Owen Schwartz
09238cd98a
Merge pull request #873 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-1931de1053
Bump zod from 3.25.46 to 3.25.56 in the prod-patch-updates group across 1 directory
2025-06-09 11:06:08 -04:00
Owen Schwartz
67b149ce4b
Merge pull request #834 from fosrl/dependabot/npm_and_yarn/tar-fs-2.1.3
Bump tar-fs from 2.1.2 to 2.1.3
2025-06-09 11:05:24 -04:00
Thijs van Loef
96151de814 improve readability 2025-06-09 13:18:22 +02:00
Thijs van Loef
f2e461a1ee improve readability 2025-06-09 13:06:08 +02:00
dependabot[bot]
8125622c98
Bump the dev-patch-updates group across 1 directory with 4 updates
Bumps the dev-patch-updates group with 4 updates in the / directory: [@types/cookie-parser](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cookie-parser), [@types/cors](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cors), [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) and [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email).


Updates `@types/cookie-parser` from 1.4.8 to 1.4.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cookie-parser)

Updates `@types/cors` from 2.8.18 to 2.8.19
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cors)

Updates `@types/react-dom` from 19.1.5 to 19.1.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `react-email` from 4.0.15 to 4.0.16
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/react-email/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/react-email@4.0.16/packages/react-email)

---
updated-dependencies:
- dependency-name: "@types/cookie-parser"
  dependency-version: 1.4.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/cors"
  dependency-version: 2.8.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/react-dom"
  dependency-version: 19.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: react-email
  dependency-version: 4.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 02:06:40 +00:00
dependabot[bot]
1a6942ccc9
Bump zod in the prod-patch-updates group across 1 directory
Bumps the prod-patch-updates group with 1 update in the / directory: [zod](https://github.com/colinhacks/zod).


Updates `zod` from 3.25.46 to 3.25.56
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.25.46...v3.25.56)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 3.25.56
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 02:05:59 +00:00
Marvin
7b0e1df778
Update dependabot.yml 2025-06-08 17:29:14 +02:00
Marvin
6f8c538086
Update Dockerfile 2025-06-08 16:57:01 +02:00
Marvin
b353a8f9b4
Update Makefile 2025-06-08 16:56:46 +02:00
Marvin
0eb35f2221
Create test.yml 2025-06-08 16:56:21 +02:00
Owen Schwartz
7d6b114d67
Merge pull request #869 from Lokowitz/fix-package
Fix - package-lock
2025-06-08 08:47:40 -04:00
Lokowitz
a169256770 update package-lock 2025-06-08 12:30:32 +00:00
Lokowitz
2e54afd72f rebuild package-lock 2025-06-08 12:26:52 +00:00
Thijs van Loef
26207bd951 add datasize helper and add correct sorting to sitestable 2025-06-08 00:07:49 +02:00
Owen
3ed681e277
Bump temp version 2025-06-06 12:16:58 -04:00
Owen
c135b5e3cf
Dont request unless its a newt 2025-06-06 12:15:15 -04:00
Owen
e648307f0b
Merge branch 'main' into dev 2025-06-06 12:04:24 -04:00
Owen Schwartz
0e4f35e87a
Merge pull request #855 from Lokowitz/main
fix - removed package-lock.json from .gitignore
2025-06-06 12:00:49 -04:00
Marvin
553dffd4ee removed package-lock.json from .gitignore
update package-lock.json
2025-06-06 07:00:07 +00:00
miloschwartz
c1fd38ac39
fix typo 2025-06-05 15:48:37 -04:00
miloschwartz
33e2798313
Merge branch 'main' into dev 2025-06-05 14:44:55 -04:00
miloschwartz
f0cb65f65c
dont import db in nextjs 2025-06-05 14:44:34 -04:00
Milo Schwartz
9bb6cb14a6
Merge pull request #843 from fosrl/dev
1.5.0
2025-06-05 12:11:47 -04:00
miloschwartz
b6f67e0f0b
Merge branch 'main' into dev 2025-06-05 12:11:37 -04:00
miloschwartz
980545c636
dont throw if fail to migration config 2025-06-05 11:55:59 -04:00
miloschwartz
92135ff9c1
minor visal adjustments to docker container view 2025-06-05 11:51:48 -04:00
Owen
ab843b1a43
Clean up unused 2025-06-04 17:42:19 -04:00
miloschwartz
4593edbb45
add get role to integration api 2025-06-04 17:28:46 -04:00
Owen
96b451843c
Update placeholder 2025-06-04 17:27:10 -04:00
Owen
54aa3ce7d8
Comment the socket status for now 2025-06-04 17:18:42 -04:00
Owen
45a70152ee
Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-06-04 17:17:56 -04:00
miloschwartz
8c5f00a446
remove .sqlite from Dockerfile 2025-06-04 17:17:18 -04:00
miloschwartz
af98610d0d
fix migration number and add allowed_headers migration 2025-06-04 17:15:11 -04:00
Owen
875ec662ad
Fix retry 2025-06-04 16:05:41 -04:00
Owen
8800ec9675
Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-06-04 16:02:52 -04:00
Owen
df4da75c57
Dont do socket on non-newt sites 2025-06-04 16:02:45 -04:00
miloschwartz
717dfae26c
look for ipv6 in brackets and fix cors headers in install config 2025-06-04 15:56:16 -04:00
Owen
58a2a9dcc9
Fix db import for pg 2025-06-04 15:24:15 -04:00
Owen
27a0df4ed4
Add migration for 1.4.0 2025-06-04 15:16:42 -04:00
Milo Schwartz
6fc6f325a7
Merge pull request #807 from pyrho/feat/auth-header
send user data to badger when authenticated
2025-06-04 12:17:23 -04:00
miloschwartz
b46e49922c
Merge branch 'dev' into postgres 2025-06-04 12:04:28 -04:00
miloschwartz
2cca561e51
support postgresql as database option 2025-06-04 12:02:07 -04:00
Owen
17919192e0
Speed up when the button shows 2025-06-03 21:04:08 -04:00
dependabot[bot]
9f979c5019
Bump tar-fs from 2.1.2 to 2.1.3
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.2 to 2.1.3.
- [Commits](https://github.com/mafintosh/tar-fs/commits)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 09:32:28 +00:00
Owen Schwartz
c3d2c34279
Merge pull request #831 from TheresaQWQ/patch-1
fix typo
2025-06-02 22:42:17 -04:00
风间苏苏
430f187fde
fix typo 2025-06-03 10:37:27 +08:00
Owen
f438d2ddbf
Watch target inputs 2025-06-02 21:13:57 -04:00
Owen
6d519af198
Rollback select packages; update fav 2025-06-02 20:47:22 -04:00
Owen Schwartz
a34e88257d
Merge pull request #823 from fosrl/dependabot/npm_and_yarn/multi-5726d3b8f4
Bump esbuild and drizzle-kit
2025-06-02 09:08:28 -04:00
Owen
ea0ab0e63c
Merge branch 'dev' into improbableone-hub-dev 2025-06-01 23:31:11 -04:00
dependabot[bot]
80375cd0dc
Bump esbuild and drizzle-kit
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.2 and updates ancestor dependency [drizzle-kit](https://github.com/drizzle-team/drizzle-orm). These dependencies need to be updated together.


Updates `esbuild` from 0.18.20 to 0.25.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.18.20...v0.25.2)

Updates `drizzle-kit` from 0.30.6 to 0.31.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/drizzle-kit@0.30.6...drizzle-kit@0.31.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.2
  dependency-type: indirect
- dependency-name: drizzle-kit
  dependency-version: 0.31.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 03:26:24 +00:00
Owen
f817ba7664
Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-06-01 23:19:18 -04:00
Owen
3398088e03
Merge branch 'Lokowitz-main' into dev 2025-06-01 23:18:58 -04:00
Owen
e586dd50f4
Merge branch 'main' of github.com:Lokowitz/pangolin into Lokowitz-main 2025-06-01 23:15:54 -04:00
Owen Schwartz
5a71c0ba65
Merge pull request #817 from PrtmPhlp/main
docker run command in detached mode
2025-06-01 22:32:22 -04:00
Marvin
f13b6abd78
Update dependabot.yml 2025-06-01 15:50:14 +02:00
Marvin
34c6b590d7
Update dependabot.yml 2025-06-01 15:47:54 +02:00
Marvin
ab797203eb
Update dependabot.yml 2025-06-01 15:46:23 +02:00
Marvin
30e8b1f0fe
Update dependabot.yml 2025-06-01 15:39:20 +02:00
dependabot[bot]
d03bee98f5
Bump yargs from 17.7.2 to 18.0.0 in the dev-major-updates group (#119)
Bumps the dev-major-updates group with 1 update: [yargs](https://github.com/yargs/yargs).


Updates `yargs` from 17.7.2 to 18.0.0
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/compare/v17.7.2...v18.0.0)

---
updated-dependencies:
- dependency-name: yargs
  dependency-version: 18.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 15:25:08 +02:00
dependabot[bot]
fa365fb7b8
Bump the dev-minor-updates group with 4 updates (#118)
Bumps the dev-minor-updates group with 4 updates: [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [drizzle-kit](https://github.com/drizzle-team/drizzle-orm) and [typescript](https://github.com/microsoft/TypeScript).


Updates `@dotenvx/dotenvx` from 1.32.0 to 1.44.1
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.32.0...v1.44.1)

Updates `@types/node` from 22.10.10 to 22.15.29
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `drizzle-kit` from 0.30.6 to 0.31.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/drizzle-kit@0.30.6...drizzle-kit@0.31.1)

Updates `typescript` from 5.7.3 to 5.8.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.8.3)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.44.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@types/node"
  dependency-version: 22.15.29
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: drizzle-kit
  dependency-version: 0.31.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript
  dependency-version: 5.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 15:16:59 +02:00
dependabot[bot]
ea1cd4b0d4
Bump the prod-minor-updates group with 27 updates (#121)
* Bump the prod-minor-updates group with 27 updates

Bumps the prod-minor-updates group with 27 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives) | `1.1.3` | `1.3.2` |
| [@radix-ui/react-radio-group](https://github.com/radix-ui/primitives) | `1.2.2` | `1.3.7` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives) | `2.1.4` | `2.2.5` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives) | `1.1.1` | `1.2.3` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives) | `1.1.2` | `1.2.5` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `1.0.6` | `1.1.2` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.20.6` | `8.21.3` |
| [arctic](https://github.com/pilcrowonpaper/arctic) | `3.6.0` | `3.7.0` |
| [axios](https://github.com/axios/axios) | `1.8.4` | `1.9.0` |
| [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `11.7.0` | `11.10.0` |
| [cmdk](https://github.com/pacocoursey/cmdk/tree/HEAD/cmdk) | `1.0.4` | `1.1.1` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.38.3` | `0.44.1` |
| [eslint](https://github.com/eslint/eslint) | `9.17.0` | `9.28.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `15.1.3` | `15.3.3` |
| [helmet](https://github.com/helmetjs/helmet) | `8.0.0` | `8.1.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.469.0` | `0.511.0` |
| [next](https://github.com/vercel/next.js) | `15.2.4` | `15.3.3` |
| [npm](https://github.com/npm/cli) | `11.2.0` | `11.4.1` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.0.0` | `19.1.0` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.1` | `19.1.6` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.0.0` | `19.1.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.1.2` | `19.1.5` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.54.2` | `7.56.4` |
| [semver](https://github.com/npm/node-semver) | `7.6.3` | `7.7.2` |
| [@types/semver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver) | `7.5.8` | `7.7.0` |
| [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.2.8` | `1.3.2` |
| [zod](https://github.com/colinhacks/zod) | `3.24.1` | `3.25.46` |


Updates `@radix-ui/react-checkbox` from 1.1.3 to 1.3.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-radio-group` from 1.2.2 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-select` from 2.1.4 to 2.2.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slot` from 1.1.1 to 1.2.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-switch` from 1.1.2 to 1.2.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@react-email/render` from 1.0.6 to 1.1.2
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/render/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/render@1.1.2/packages/render)

Updates `@tanstack/react-table` from 8.20.6 to 8.21.3
- [Release notes](https://github.com/TanStack/table/releases)
- [Commits](https://github.com/TanStack/table/commits/v8.21.3/packages/react-table)

Updates `arctic` from 3.6.0 to 3.7.0
- [Release notes](https://github.com/pilcrowonpaper/arctic/releases)
- [Commits](https://github.com/pilcrowonpaper/arctic/compare/v3.6.0...v3.7.0)

Updates `axios` from 1.8.4 to 1.9.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.4...v1.9.0)

Updates `better-sqlite3` from 11.7.0 to 11.10.0
- [Release notes](https://github.com/WiseLibs/better-sqlite3/releases)
- [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v11.7.0...v11.10.0)

Updates `cmdk` from 1.0.4 to 1.1.1
- [Release notes](https://github.com/pacocoursey/cmdk/releases)
- [Commits](https://github.com/pacocoursey/cmdk/commits/v1.1.1/cmdk)

Updates `drizzle-orm` from 0.38.3 to 0.44.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.38.3...0.44.1)

Updates `eslint` from 9.17.0 to 9.28.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.17.0...v9.28.0)

Updates `eslint-config-next` from 15.1.3 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/packages/eslint-config-next)

Updates `helmet` from 8.0.0 to 8.1.0
- [Changelog](https://github.com/helmetjs/helmet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/helmetjs/helmet/compare/v8.0.0...v8.1.0)

Updates `lucide-react` from 0.469.0 to 0.511.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.511.0/packages/lucide-react)

Updates `next` from 15.2.4 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.2.4...v15.3.3)

Updates `npm` from 11.2.0 to 11.4.1
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.2.0...v11.4.1)

Updates `react` from 19.0.0 to 19.1.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react)

Updates `@types/react` from 19.1.1 to 19.1.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.0.0 to 19.1.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react-dom)

Updates `@types/react-dom` from 19.1.2 to 19.1.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `react-hook-form` from 7.54.2 to 7.56.4
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.54.2...v7.56.4)

Updates `semver` from 7.6.3 to 7.7.2
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.6.3...v7.7.2)

Updates `@types/semver` from 7.5.8 to 7.7.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

Updates `tw-animate-css` from 1.2.8 to 1.3.2
- [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases)
- [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.2.8...v1.3.2)

Updates `zod` from 3.24.1 to 3.25.46
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.24.1...v3.25.46)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@radix-ui/react-radio-group"
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.2.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.2.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@react-email/render"
  dependency-version: 1.1.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@tanstack/react-table"
  dependency-version: 8.21.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: arctic
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: axios
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: better-sqlite3
  dependency-version: 11.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: cmdk
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: drizzle-orm
  dependency-version: 0.44.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint
  dependency-version: 9.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint-config-next
  dependency-version: 15.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: helmet
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.511.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: next
  dependency-version: 15.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: npm
  dependency-version: 11.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react
  dependency-version: 19.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@types/react"
  dependency-version: 19.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: prod-minor-updates
- dependency-name: react-dom
  dependency-version: 19.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@types/react-dom"
  dependency-version: 19.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.56.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: semver
  dependency-version: 7.7.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@types/semver"
  dependency-version: 7.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: tw-animate-css
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: zod
  dependency-version: 3.25.46
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* modified:   package-lock.json
	modified:   package.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marvin <127591405+Lokowitz@users.noreply.github.com>
2025-06-01 15:07:49 +02:00
dependabot[bot]
be0c7444e9
Bump node from 20-alpine to 24-alpine in the major-updates group (#111)
Bumps the major-updates group with 1 update: node.


Updates `node` from 20-alpine to 24-alpine

---
updated-dependencies:
- dependency-name: node
  dependency-version: 24-alpine
  dependency-type: direct:production
  dependency-group: major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 14:12:08 +02:00
dependabot[bot]
858c809514
Bump the dev-patch-updates group with 8 updates (#117)
Bumps the dev-patch-updates group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.1.4` | `4.1.8` |
| [@types/cors](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cors) | `2.8.17` | `2.8.18` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.2` | `0.25.5` |
| [postcss](https://github.com/postcss/postcss) | `8.5.1` | `8.5.4` |
| [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email) | `4.0.6` | `4.0.15` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.4` | `4.1.8` |
| [tsc-alias](https://github.com/justkey007/tsc-alias) | `1.8.10` | `1.8.16` |
| [tsx](https://github.com/privatenumber/tsx) | `4.19.3` | `4.19.4` |


Updates `@tailwindcss/postcss` from 4.1.4 to 4.1.8
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.8/packages/@tailwindcss-postcss)

Updates `@types/cors` from 2.8.17 to 2.8.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cors)

Updates `esbuild` from 0.25.2 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.2...v0.25.5)

Updates `postcss` from 8.5.1 to 8.5.4
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.1...8.5.4)

Updates `react-email` from 4.0.6 to 4.0.15
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/react-email/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/react-email@4.0.15/packages/react-email)

Updates `tailwindcss` from 4.1.4 to 4.1.8
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.8/packages/tailwindcss)

Updates `tsc-alias` from 1.8.10 to 1.8.16
- [Release notes](https://github.com/justkey007/tsc-alias/releases)
- [Commits](https://github.com/justkey007/tsc-alias/compare/v1.8.10...v1.8.16)

Updates `tsx` from 4.19.3 to 4.19.4
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.19.3...v4.19.4)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/cors"
  dependency-version: 2.8.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: postcss
  dependency-version: 8.5.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: react-email
  dependency-version: 4.0.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tsc-alias
  dependency-version: 1.8.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tsx
  dependency-version: 4.19.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 14:11:51 +02:00
dependabot[bot]
10ff2c8a65
Bump the prod-patch-updates group with 19 updates (#120)
Bumps the prod-patch-updates group with 19 updates:

| Package | From | To |
| --- | --- | --- |
| [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi) | `7.3.0` | `7.3.2` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.10` |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.11` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives) | `1.1.4` | `1.1.14` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives) | `2.1.4` | `2.1.15` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives) | `2.1.1` | `2.1.7` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives) | `1.1.4` | `1.1.14` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives) | `1.1.4` | `1.1.7` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives) | `1.1.1` | `1.1.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.12` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives) | `1.2.4` | `1.2.14` |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `0.0.36` | `0.0.41` |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `1.0.4` | `1.0.5` |
| [glob](https://github.com/isaacs/node-glob) | `11.0.0` | `11.0.2` |
| [input-otp](https://github.com/guilhermerodz/input-otp/tree/HEAD/packages/input-otp) | `1.4.1` | `1.4.2` |
| [next-themes](https://github.com/pacocoursey/next-themes) | `0.4.4` | `0.4.6` |
| [ws](https://github.com/websockets/ws) | `8.18.0` | `8.18.2` |
| [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws) | `8.5.13` | `8.18.1` |
| [zod-validation-error](https://github.com/causaly/zod-validation-error) | `3.4.0` | `3.4.1` |


Updates `@asteasolutions/zod-to-openapi` from 7.3.0 to 7.3.2
- [Release notes](https://github.com/asteasolutions/zod-to-openapi/releases)
- [Commits](https://github.com/asteasolutions/zod-to-openapi/compare/v7.3.0...v7.3.2)

Updates `@radix-ui/react-avatar` from 1.1.2 to 1.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-collapsible` from 1.1.2 to 1.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dialog` from 1.1.4 to 1.1.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dropdown-menu` from 2.1.4 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-label` from 2.1.1 to 2.1.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-popover` from 1.1.4 to 1.1.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-progress` from 1.1.4 to 1.1.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-separator` from 1.1.1 to 1.1.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tabs` from 1.1.2 to 1.1.12
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-toast` from 1.2.4 to 1.2.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@react-email/components` from 0.0.36 to 0.0.41
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/components@0.0.41/packages/components)

Updates `@react-email/tailwind` from 1.0.4 to 1.0.5
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@1.0.5/packages/tailwind)

Updates `glob` from 11.0.0 to 11.0.2
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.0...v11.0.2)

Updates `input-otp` from 1.4.1 to 1.4.2
- [Changelog](https://github.com/guilhermerodz/input-otp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guilhermerodz/input-otp/commits/HEAD/packages/input-otp)

Updates `next-themes` from 0.4.4 to 0.4.6
- [Release notes](https://github.com/pacocoursey/next-themes/releases)
- [Commits](https://github.com/pacocoursey/next-themes/compare/v0.4.4...v0.4.6)

Updates `ws` from 8.18.0 to 8.18.2
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.0...8.18.2)

Updates `@types/ws` from 8.5.13 to 8.18.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

Updates `zod-validation-error` from 3.4.0 to 3.4.1
- [Release notes](https://github.com/causaly/zod-validation-error/releases)
- [Changelog](https://github.com/causaly/zod-validation-error/blob/main/CHANGELOG.md)
- [Commits](https://github.com/causaly/zod-validation-error/compare/v3.4.0...v3.4.1)

---
updated-dependencies:
- dependency-name: "@asteasolutions/zod-to-openapi"
  dependency-version: 7.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/components"
  dependency-version: 0.0.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/tailwind"
  dependency-version: 1.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: glob
  dependency-version: 11.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: input-otp
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: next-themes
  dependency-version: 0.4.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: ws
  dependency-version: 8.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@types/ws"
  dependency-version: 8.18.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: prod-patch-updates
- dependency-name: zod-validation-error
  dependency-version: 3.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 14:08:55 +02:00
PrtmPhlp
167d0b6867
unnecessary file 2025-06-01 12:19:09 +02:00
PrtmPhlp
8c121daf6c
docker run command in detached mode 2025-06-01 12:15:14 +02:00
Marvin
a23d437bd3
Create dependabot.yml 2025-06-01 11:01:55 +02:00
Owen
cd280d1396
Also start the service 2025-05-31 11:02:53 -04:00
Owen
d18200739a
Check if docker is running 2025-05-31 11:01:03 -04:00
Owen
a62b2e8d10
Use 41 for dnf5 2025-05-31 10:56:38 -04:00
Owen Schwartz
c92069a1f4
Merge pull request #796 from socheatsok78/non-root-installer
Allow installer to run without requires `sudo`
2025-05-31 10:48:47 -04:00
Damien Rajon
c5e37c1608 send user data to badger when authenticated 2025-05-30 20:37:21 +02:00
Rajesh V
948eb7f6d0 docker socket 2025-05-29 22:34:05 +05:30
miloschwartz
62a0104e70
Merge branch 'dev' into postgres 2025-05-29 12:09:56 -04:00
miloschwartz
6dd8db5cd1
add new logo 2025-05-29 12:09:49 -04:00
Socheat Sok
9ea7275371
Ensure installer check if current user is in docker group 2025-05-29 22:49:42 +07:00
Socheat Sok
c997b8625f
Re: "Allow installer to run without sudo & only need it when need to install Docker" 2025-05-29 21:55:50 +07:00
Socheat Sok
6f3514199a
Revert "Allow installer to run without sudo & only need it when need to install Docker"
This reverts commit 56fd366a7d.
2025-05-29 21:45:57 +07:00
Owen
0cfc4d7dad
Update for new dnf 2025-05-29 10:40:15 -04:00
Socheat Sok
56fd366a7d
Allow installer to run without sudo & only need it when need to install Docker 2025-05-29 16:05:31 +07:00
Owen
23b5dcfbed
Update readme 2025-05-27 20:47:23 -04:00
miloschwartz
30ebbaaef0
Merge branch 'dev' into postgres 2025-05-27 17:19:43 -04:00
Owen Schwartz
b467d6afa1
Merge pull request #779 from jpgnz/main
README.md - Append https:// to geoblock
2025-05-24 23:59:11 -04:00
James Graham
373441b7ab Fix geolock url in README.md
Geoblock url needed https:// appended
2025-05-25 13:36:44 +12:00
Owen Schwartz
825730052b
Merge pull request #736 from TuncTaylan/traefik-log-rotation
traefik log rotation with default values
2025-05-16 10:28:48 -04:00
Owen Schwartz
edc8716297
Merge pull request #735 from TuncTaylan/traefik-update
update to traefik v3.4.0
2025-05-16 10:27:50 -04:00
Taylan
3ee4aaf194 log rotation with default values 2025-05-16 09:20:40 +02:00
Taylan
b9a5d486b9 update to v3.4.0 2025-05-16 09:16:49 +02:00
a727626807
Merge branch 'main' into oss 2025-05-14 12:09:28 +02:00
miloschwartz
dc66ebeed6
Merge branch 'dev' into postgres 2025-05-13 15:08:05 -04:00
Milo Schwartz
1f584bf3e8
Merge pull request #717 from fosrl/dev
1.4.0
2025-05-13 11:12:03 -04:00
miloschwartz
5b0200154a
add feature parity 2025-05-13 11:09:38 -04:00
miloschwartz
1e55d96376
add different driver 2025-05-12 17:21:03 -04:00
Milo Schwartz
a512148348
Merge pull request #701 from fosrl/dev
1.3.2
2025-05-10 11:30:07 -04:00
miloschwartz
d9eccd6c13
bump version 2025-05-10 11:28:22 -04:00
miloschwartz
492669f68a
set default congig values 2025-05-09 18:32:14 -04:00
miloschwartz
caded23b51
allow root path 2025-05-09 17:37:55 -04:00
miloschwartz
e9cc48a3ae
fix bug causing duplicate targets 2025-05-09 17:18:42 -04:00
miloschwartz
4ed98c227b
fix setting tlsServerName and hostHeader conflict 2025-05-09 17:12:01 -04:00
miloschwartz
f66fb7d4a3
fix justification for profile icon 2025-05-09 17:09:22 -04:00
miloschwartz
f25990a9a7
add id token and claims to debug logs 2025-05-09 16:46:51 -04:00
miloschwartz
21d5b67ef1
Merge branch 'main' into dev 2025-05-09 16:44:09 -04:00
Milo Schwartz
198810121c
Update README.md 2025-05-09 16:44:01 -04:00
Owen
83c0379c6b
Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-05-03 22:04:29 -04:00
Owen
9a167b5acb
Dont overwrite the secret and crowdsec vars 2025-05-02 14:16:10 -04:00
300 changed files with 12577 additions and 4693 deletions

35
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,35 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
groups:
patch-updates:
update-types:
- "patch"
minor-updates:
update-types:
- "minor"

49
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: Run Tests
on:
pull_request:
branches:
- main
- dev
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Copy config file
run: cp config/config.example.yml config/config.yml
- name: Install dependencies
run: npm ci
- name: Generate database migrations
run: npm run db:sqlite:generate
- name: Apply database migrations
run: npm run db:sqlite:push
- name: Start app in background
run: nohup npm run dev &
- name: Wait for app availability
run: |
for i in {1..5}; do
if curl --silent --fail http://localhost:3002/auth/login; then
echo "App is up"
exit 0
fi
echo "Waiting for the app... attempt $i"
sleep 5
done
echo "App failed to start"
exit 1
- name: Build Docker image
run: make build

View file

@ -2,14 +2,17 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ # COPY package.json package-lock.json ./
RUN npm install COPY package*.json ./
RUN npm ci
COPY . . COPY . .
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schemas/ --out init RUN echo 'export * from "./sqlite";' > server/db/index.ts
RUN npm run build RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init
RUN npm run build:sqlite
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
@ -19,8 +22,8 @@ WORKDIR /app
RUN apk add --no-cache curl RUN apk add --no-cache curl
# COPY package.json package-lock.json ./ # COPY package.json package-lock.json ./
COPY package.json ./ COPY package*.json ./
RUN npm install --omit=dev && npm cache clean --force RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
@ -31,4 +34,4 @@ COPY server/db/names.json ./dist/names.json
COPY public ./public COPY public ./public
CMD ["npm", "start"] CMD ["npm", "run", "start:sqlite"]

37
Dockerfile.pg Normal file
View file

@ -0,0 +1,37 @@
FROM node:20-alpine AS builder
WORKDIR /app
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci
COPY . .
RUN echo 'export * from "./pg";' > server/db/index.ts
RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init
RUN npm run build:pg
FROM node:20-alpine AS runner
WORKDIR /app
# Curl used for the health checks
RUN apk add --no-cache curl
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init
COPY server/db/names.json ./dist/names.json
COPY public ./public
CMD ["npm", "run", "start:pg"]

View file

@ -1,5 +1,3 @@
Copyright (c) 2025 Fossorial, LLC.
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007

View file

@ -1,10 +1,14 @@
.PHONY: build build-release build-arm build-x86 test clean
build-release: build-release:
@if [ -z "$(tag)" ]; then \ @if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \ echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \ exit 1; \
fi fi
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push .
build-arm: build-arm:
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .

View file

@ -1,15 +1,13 @@
<div align="center"> <div align="center">
<h2 align="center"><a href="https://fossorial.io"><img alt="pangolin" src="public/logo//word_mark.png" width="400" /></a></h2> <h2>
<picture>
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.fossorial.io/) <source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
[![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) <img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) </picture>
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) </h2>
[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
</div> </div>
<h3 align="center">Tunneled Mesh Reverse Proxy Server with Access Control</h3> <h4 align="center">Tunneled Reverse Proxy Server with Access Control</h4>
<div align="center"> <div align="center">
_Your own self-hosted zero trust tunnel._ _Your own self-hosted zero trust tunnel._
@ -30,6 +28,12 @@ _Your own self-hosted zero trust tunnel._
Contact Us Contact Us
</a> </a>
</h5> </h5>
[![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin)
![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square)
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
</div> </div>
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
@ -86,7 +90,7 @@ will be reimplemented under the AGPL license.
### Modular Design ### Modular Design
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](github.com/PascalMinder/geoblock). - Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
- **Automatically install and configure Crowdsec via Pangolin's installer script.** - **Automatically install and configure Crowdsec via Pangolin's installer script.**
- Attach as many sites to the central server as you wish. - Attach as many sites to the central server as you wish.
@ -99,19 +103,19 @@ will be reimplemented under the AGPL license.
- Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs. - Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs.
> [!TIP] > [!TIP]
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal! > Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone. > We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
2. **Domain Configuration**: 1. **Domain Configuration**:
- Point your domain name to the VPS and configure Pangolin with your preferred settings. - Point your domain name to the VPS and configure Pangolin with your preferred settings.
3. **Connect Private Sites**: 2. **Connect Private Sites**:
- Install Newt or use another WireGuard client on private sites. - Install Newt or use another WireGuard client on private sites.
- Automatically establish a connection from these sites to the central server. - Automatically establish a connection from these sites to the central server.
4. **Expose Resources**: 3. **Expose Resources**:
- Add resources to the central server and configure access control rules. - Add resources to the central server and configure access control rules.
- Access these resources securely from anywhere. - Access these resources securely from anywhere.

View file

@ -35,7 +35,7 @@ services:
- 80:80 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode
traefik: traefik:
image: traefik:v3.3.3 image: traefik:v3.4.0
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service network_mode: service:gerbil # Ports appear on the gerbil service

12
drizzle.pg.config.ts Normal file
View file

@ -0,0 +1,12 @@
import { defineConfig } from "drizzle-kit";
import path from "path";
export default defineConfig({
dialect: "postgresql",
schema: path.join("server", "db", "pg", "schema.ts"),
out: path.join("server", "migrations"),
verbose: true,
dbCredentials: {
url: process.env.DATABASE_URL as string
}
});

View file

@ -4,7 +4,7 @@ import path from "path";
export default defineConfig({ export default defineConfig({
dialect: "sqlite", dialect: "sqlite",
schema: path.join("server", "db", "schemas"), schema: path.join("server", "db", "sqlite", "schema.ts"),
out: path.join("server", "migrations"), out: path.join("server", "migrations"),
verbose: true, verbose: true,
dbCredentials: { dbCredentials: {

View file

@ -1,9 +1,12 @@
// eslint.config.js import tseslint from 'typescript-eslint';
export default [
export default tseslint.config(
tseslint.configs.recommended,
{ {
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
rules: { rules: {
semi: "error", semi: "error",
"prefer-const": "error" "prefer-const": "error"
} }
} }
]; );

View file

@ -26,7 +26,7 @@ server:
cors: cors:
origins: ["https://{{.DashboardDomain}}"] origins: ["https://{{.DashboardDomain}}"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
traefik: traefik:

View file

@ -21,6 +21,10 @@ experimental:
log: log:
level: "INFO" level: "INFO"
format: "json" # Log format changed to json for better parsing format: "json" # Log format changed to json for better parsing
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true
accessLog: # We enable access logs as json accessLog: # We enable access logs as json
filePath: "/var/log/traefik/access.log" filePath: "/var/log/traefik/access.log"

View file

@ -35,7 +35,7 @@ services:
- 80:80 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode
{{end}} {{end}}
traefik: traefik:
image: traefik:v3.3.6 image: traefik:v3.4.1
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
{{if .InstallGerbil}} {{if .InstallGerbil}}

View file

@ -18,6 +18,10 @@ experimental:
log: log:
level: "INFO" level: "INFO"
format: "common" format: "common"
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:

View file

@ -9,6 +9,7 @@ import (
"io/fs" "io/fs"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -17,6 +18,7 @@ import (
"time" "time"
"unicode" "unicode"
"math/rand" "math/rand"
"strconv"
"golang.org/x/term" "golang.org/x/term"
) )
@ -57,21 +59,30 @@ type Config struct {
func main() { func main() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
// check if the user is root // check if docker is not installed and the user is root
if !isDockerInstalled() {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
fmt.Println("This script must be run as root") fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
os.Exit(1)
}
}
// check if the user is in the docker group (linux only)
if !isUserInDockerGroup() {
fmt.Println("You are not in the docker group.")
fmt.Println("The installer will not be able to run docker commands without running it as root.")
os.Exit(1) os.Exit(1)
} }
var config Config var config Config
config.DoCrowdsecInstall = false
config.Secret = generateRandomSecretKey()
// check if there is already a config file // check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil { if _, err := os.Stat("config/config.yml"); err != nil {
config = collectUserInput(reader) config = collectUserInput(reader)
loadVersions(&config) loadVersions(&config)
config.DoCrowdsecInstall = false
config.Secret = generateRandomSecretKey()
if err := createConfigFiles(config); err != nil { if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err) fmt.Printf("Error creating config files: %v\n", err)
@ -83,6 +94,27 @@ func main() {
if !isDockerInstalled() && runtime.GOOS == "linux" { if !isDockerInstalled() && runtime.GOOS == "linux" {
if readBool(reader, "Docker is not installed. Would you like to install it?", true) { if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
installDocker() installDocker()
// try to start docker service but ignore errors
if err := startDockerService(); err != nil {
fmt.Println("Error starting Docker service:", err)
} else {
fmt.Println("Docker service started successfully!")
}
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
fmt.Println("Waiting for Docker to start...")
for i := 0; i < 5; i++ {
if isDockerRunning() {
fmt.Println("Docker is running!")
break
}
fmt.Println("Docker is not running yet, waiting...")
time.Sleep(2 * time.Second)
}
if !isDockerRunning() {
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
os.Exit(1)
}
fmt.Println("Docker installed successfully!")
} }
} }
@ -438,11 +470,31 @@ func installDocker() error {
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
`, dockerArch)) `, dockerArch))
case strings.Contains(osRelease, "ID=fedora"): case strings.Contains(osRelease, "ID=fedora"):
installCmd = exec.Command("bash", "-c", ` // Detect Fedora version to handle DNF 5 changes
versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'")
versionOutput, err := versionCmd.Output()
var fedoraVersion int
if err == nil {
if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil {
fedoraVersion = v
}
}
// Use appropriate DNF syntax based on version
var repoCmd string
if fedoraVersion >= 41 {
// DNF 5 syntax for Fedora 41+
repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo"
} else {
// DNF 4 syntax for Fedora < 41
repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
}
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
dnf -y install dnf-plugins-core && dnf -y install dnf-plugins-core &&
dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && %s &&
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
`) `, repoCmd))
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
installCmd = exec.Command("bash", "-c", ` installCmd = exec.Command("bash", "-c", `
zypper install -y docker docker-compose && zypper install -y docker docker-compose &&
@ -466,11 +518,26 @@ func installDocker() error {
default: default:
return fmt.Errorf("unsupported Linux distribution") return fmt.Errorf("unsupported Linux distribution")
} }
installCmd.Stdout = os.Stdout installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr installCmd.Stderr = os.Stderr
return installCmd.Run() return installCmd.Run()
} }
func startDockerService() error {
if runtime.GOOS == "linux" {
cmd := exec.Command("systemctl", "enable", "--now", "docker")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
} else if runtime.GOOS == "darwin" {
// On macOS, Docker is usually started via the Docker Desktop application
fmt.Println("Please start Docker Desktop manually on macOS.")
return nil
}
return fmt.Errorf("unsupported operating system for starting Docker service")
}
func isDockerInstalled() bool { func isDockerInstalled() bool {
cmd := exec.Command("docker", "--version") cmd := exec.Command("docker", "--version")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@ -479,6 +546,43 @@ func isDockerInstalled() bool {
return true return true
} }
func isUserInDockerGroup() bool {
if runtime.GOOS == "darwin" {
// Docker group is not applicable on macOS
// So we assume that the user can run Docker commands
return true
}
if os.Geteuid() == 0 {
return true // Root user can run Docker commands anyway
}
// Check if the current user is in the docker group
if dockerGroup, err := user.LookupGroup("docker"); err == nil {
if currentUser, err := user.Current(); err == nil {
if currentUserGroupIds, err := currentUser.GroupIds(); err == nil {
for _, groupId := range currentUserGroupIds {
if groupId == dockerGroup.Gid {
return true
}
}
}
}
}
// Eventually, if any of the checks fail, we assume the user cannot run Docker commands
return false
}
// isDockerRunning checks if the Docker daemon is running by using the `docker info` command.
func isDockerRunning() bool {
cmd := exec.Command("docker", "info")
if err := cmd.Run(); err != nil {
return false
}
return true
}
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied // executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
func executeDockerComposeCommandWithArgs(args ...string) error { func executeDockerComposeCommandWithArgs(args ...string) error {
var cmd *exec.Cmd var cmd *exec.Cmd

View file

@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true
}, },
output: "standalone" output: "standalone"
}; };

6092
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,100 +12,108 @@
"license": "SEE LICENSE IN LICENSE AND README.md", "license": "SEE LICENSE IN LICENSE AND README.md",
"scripts": { "scripts": {
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts", "dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
"db:generate": "drizzle-kit generate", "db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
"db:push": "npx tsx server/db/migrate.ts", "db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
"db:studio": "drizzle-kit studio", "db:pg:push": "npx tsx server/db/pg/migrate.ts",
"build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.ts -o dist/migrations.mjs", "db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
"start": "NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
"db:clear-migrations": "rm -rf server/migrations",
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
"start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
"start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
"email": "email dev --dir server/emails/templates --port 3005" "email": "email dev --dir server/emails/templates --port 3005"
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.0", "@asteasolutions/zod-to-openapi": "^7.3.2",
"@hookform/resolvers": "3.9.1", "@hookform/resolvers": "3.9.1",
"@node-rs/argon2": "2.0.2", "@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.2", "@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "1.1.3", "@radix-ui/react-checkbox": "1.3.2",
"@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-collapsible": "1.1.11",
"@radix-ui/react-dialog": "1.1.4", "@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "2.1.4", "@radix-ui/react-dropdown-menu": "2.1.15",
"@radix-ui/react-icons": "1.3.2", "@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.1", "@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "1.1.4", "@radix-ui/react-popover": "1.1.14",
"@radix-ui/react-progress": "^1.1.4", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "1.2.2", "@radix-ui/react-radio-group": "1.3.7",
"@radix-ui/react-select": "2.1.4", "@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-separator": "1.1.1", "@radix-ui/react-select": "2.2.5",
"@radix-ui/react-slot": "1.1.1", "@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-switch": "1.1.2", "@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-tabs": "1.1.2", "@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-toast": "1.2.4", "@radix-ui/react-tabs": "1.1.12",
"@react-email/components": "0.0.36", "@radix-ui/react-toast": "1.2.14",
"@react-email/render": "^1.0.6", "@react-email/components": "0.0.41",
"@react-email/tailwind": "1.0.4", "@react-email/render": "^1.1.2",
"@react-email/tailwind": "1.0.5",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.20.6", "@tanstack/react-table": "8.21.3",
"arctic": "^3.6.0", "arctic": "^3.7.0",
"axios": "1.8.4", "axios": "1.9.0",
"better-sqlite3": "11.7.0", "better-sqlite3": "11.7.0",
"canvas-confetti": "1.9.3", "canvas-confetti": "1.9.3",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"cmdk": "1.0.4", "cmdk": "1.1.1",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"cors": "2.8.5", "cors": "2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"drizzle-orm": "0.38.3", "drizzle-orm": "0.38.3",
"eslint": "9.17.0", "eslint": "9.28.0",
"eslint-config-next": "15.1.3", "eslint-config-next": "15.3.3",
"express": "4.21.2", "express": "4.21.2",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.0",
"glob": "11.0.0", "glob": "11.0.2",
"helmet": "8.0.0", "helmet": "8.1.0",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"input-otp": "1.4.1", "input-otp": "1.4.2",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "0.469.0", "lucide-react": "0.511.0",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.2.4", "next": "15.3.3",
"next-themes": "0.4.4", "next-themes": "0.4.6",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.16", "nodemailer": "6.9.16",
"npm": "^11.2.0", "npm": "^11.4.1",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "^8.16.0",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.0.0", "react": "19.1.0",
"react-dom": "19.0.0", "react-dom": "19.1.0",
"react-easy-sort": "^1.6.0", "react-easy-sort": "^1.6.0",
"react-hook-form": "7.54.2", "react-hook-form": "7.56.4",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"rebuild": "0.1.2", "rebuild": "0.1.2",
"semver": "7.6.3", "semver": "7.7.2",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"tailwind-merge": "2.6.0", "tailwind-merge": "2.6.0",
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.3.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vaul": "1.1.2", "vaul": "1.1.2",
"winston": "3.17.0", "winston": "3.17.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.18.0", "ws": "8.18.2",
"zod": "3.24.1", "zod": "3.25.56",
"zod-validation-error": "3.4.0" "zod-validation-error": "3.4.1"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.32.0", "@dotenvx/dotenvx": "1.44.2",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@tailwindcss/postcss": "^4.1.3", "@tailwindcss/postcss": "^4.1.8",
"@types/better-sqlite3": "7.6.12", "@types/better-sqlite3": "7.6.12",
"@types/cookie-parser": "1.4.8", "@types/cookie-parser": "1.4.9",
"@types/cors": "2.8.17", "@types/cors": "2.8.19",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/express": "5.0.0", "@types/express": "5.0.0",
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
@ -113,22 +121,23 @@
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22", "@types/node": "^22",
"@types/nodemailer": "6.4.17", "@types/nodemailer": "6.4.17",
"@types/react": "19.1.1", "@types/react": "19.1.7",
"@types/react-dom": "19.1.2", "@types/react-dom": "19.1.6",
"@types/semver": "7.5.8", "@types/semver": "7.7.0",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/ws": "8.5.13", "@types/ws": "8.18.1",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"drizzle-kit": "0.30.6", "drizzle-kit": "0.31.1",
"esbuild": "0.25.2", "esbuild": "0.25.5",
"esbuild-node-externals": "1.18.0", "esbuild-node-externals": "1.18.0",
"postcss": "^8", "postcss": "^8",
"react-email": "4.0.6", "react-email": "4.0.16",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.16",
"tsx": "4.19.3", "tsx": "4.19.4",
"typescript": "^5", "typescript": "^5",
"yargs": "17.7.2" "typescript-eslint": "^8.34.0",
"yargs": "18.0.0"
}, },
"overrides": { "overrides": {
"emblor": { "emblor": {

View file

@ -1,22 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="900.82861"
height="955.20648"
viewBox="0 0 238.34422 252.7317"
version="1.1" version="1.1"
x="0px" id="svg420"
y="0px" inkscape:export-filename="logo.svg"
viewBox="0 0 399.99999 400.00002" inkscape:export-xdpi="221.14999"
enable-background="new 0 0 419.528 419.528" inkscape:export-ydpi="221.14999"
xml:space="preserve"
id="svg52"
sodipodi:docname="noun-pangolin-1798092.svg"
width="400"
height="400"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs xmlns:svg="http://www.w3.org/2000/svg">
id="defs56" /><sodipodi:namedview <sodipodi:namedview
id="namedview54" id="namedview422"
pagecolor="#ffffff" pagecolor="#ffffff"
bordercolor="#666666" bordercolor="#666666"
borderopacity="1.0" borderopacity="1.0"
@ -24,15 +23,18 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
showgrid="false" inkscape:document-units="mm"
inkscape:zoom="1.9583914" showgrid="false" />
inkscape:cx="209.86611" <defs
inkscape:cy="262.20499" id="defs417" />
inkscape:window-width="3840" <g
inkscape:window-height="2136" inkscape:label="Layer 1"
inkscape:window-x="0" inkscape:groupmode="layer"
inkscape:window-y="0" id="layer1"
inkscape:window-maximized="1" transform="translate(-13.119542,-5.9258171)">
inkscape:current-layer="svg52" /><path <path
d="m 62.232921,184.91974 c 0,2.431 -1.97,4.402 -4.399,4.402 -2.429,0 -4.399,-1.972 -4.399,-4.402 0,-2.429 1.97,-4.399 4.399,-4.399 2.429,-10e-4 4.399,1.97 4.399,4.399 z m 58.993999,-4.821 c -25.943999,-2.826 -38.978999,7.453 -71.181999,31.357 -27.572,20.467 -32.767,4.381 -31.748,-2.614 1.499,-10.282 25.222,-58.573 48.079,-88.461 28.273,7.34 49.869999,30.727 54.850999,59.718 z m -55.915999,4.821 c 0,-4.131 -3.349,-7.478 -7.478,-7.478 -4.129,0 -7.478,3.347 -7.478,7.478 0,4.131 3.349,7.481 7.478,7.481 4.13,0 7.478,-3.35 7.478,-7.481 z m -15.032,48.424 -0.234,14.041 20.413,22.687 -9.818,7.353 33.306,27.492 -11.759,8.124 42.631999,19.939 -10.825,9.747 48.291,8.078 -7.526,10.307 48.758,-4.531 -3.997,11.725 53.916,-18.153 -2.76,13.357 48.077,-34.345 1.479,13.562 34.087,-48.576 7.478,14.206 15.187,-58.89 10.391,8.533 -2.14,-57.884 13.814,5.13 -21.082,-51.204 13.404,0.048 -33.696,-42.131 15.312,-1.366 -47.026,-32.831002 14.255,-8.399 -54.817,-14.682 9.257,-11.695 -49.625,0.352 0.6,-13.337 -38.537,14.084 -1.597,-12.689 -29.984,21.429 -6.446,-10.852 -22.59,26.504 -7.021,-9.572 -18.923,30.294 -9.595999,-8.744 -16.754,30.138002 c 31.509999,10.197 54.979999,37.951 59.126999,71.547 0.404,0.087 -22.37,31.257 10.955,57.85 -0.576,-2.985 -6.113,-53.902 47.496,-57.61 26.668,-1.844 48.4,21.666 48.4,48.399 0,8.184 -2.05,15.883 -5.636,22.64 -15.927,29.611 -64.858,30.755 -80.429,30.596 -45.154,-0.459 -104.051999,-51.521 -104.051999,-51.521 z" d="m 213.66176,90.072122 c 4.95655,0 8.97383,4.018046 8.97383,8.973827 0,4.956581 -4.01728,8.974621 -8.97383,8.974621 -4.95657,0 -8.97462,-4.01804 -8.97462,-8.974621 0,-4.955781 4.01805,-8.973827 8.97462,-8.973827 z m 35.2316,37.450998 c -0.90048,29.80928 -23.66033,69.21262 -54.51292,79.34466 -36.04206,11.836 -63.40991,-5.92226 -72.08409,-26.74061 -6.75754,-16.21966 -1.65117,-35.62363 10.96266,-43.83669 10.6506,-6.93533 30.48543,-8.76736 47.15454,2.19144 -5.85627,-15.34246 -21.62491,-25.4256 -35.59101,-28.49424 -13.96613,-3.06867 -28.38324,0.43858 -38.74504,5.69946 13.29071,-14.68572 44.40801,-28.946049 78.24077,-10.95958 22.67676,12.05491 32.43775,28.93208 42.0489,51.72763 C 251.59637,117.87858 234.026,71.411066 203.39074,43.794029 172.15544,15.636686 129.95516,4.340214 97.668803,6.103155 108.32483,12.678273 120.84625,22.06586 132.41209,33.053363 81.298533,26.697169 39.174705,38.314245 13.119542,73.749217 27.67508,70.878527 46.868833,69.073666 65.974711,70.016861 28.737658,96.252107 7.1124298,140.38147 18.105298,186.43137 c 6.718497,-11.74129 16.767711,-25.84558 28.726275,-38.62863 -3.677175,34.36994 1.42836,80.83745 45.62293,110.85478 -2.25587,-9.42394 -4.08014,-20.88443 -4.91466,-33.0154 20.673197,16.1282 50.685067,29.42205 87.917917,20.24096 65.77679,-16.21975 83.34719,-79.78335 73.4356,-118.35996"
id="path46" /></svg> style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0776283"
id="path32" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,39 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="900.82861"
height="955.20648"
viewBox="0 0 238.34422 252.7317"
version="1.1" version="1.1"
x="0px" id="svg420"
y="0px"
viewBox="0 0 399.99999 400.00002"
enable-background="new 0 0 419.528 419.528"
xml:space="preserve"
id="svg52"
sodipodi:docname="pangolin_orange.svg"
width="400"
height="400"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs xmlns:svg="http://www.w3.org/2000/svg">
id="defs56" /><sodipodi:namedview <defs
id="namedview54" id="defs417" />
pagecolor="#ffffff" <g
bordercolor="#666666" id="layer1"
borderopacity="1.0" transform="translate(-13.119542,-5.9258171)">
inkscape:showpageshadow="2" <path
inkscape:pageopacity="0.0" d="m 213.66176,90.072122 c 4.95655,0 8.97383,4.018046 8.97383,8.973827 0,4.956581 -4.01728,8.974621 -8.97383,8.974621 -4.95657,0 -8.97462,-4.01804 -8.97462,-8.974621 0,-4.955781 4.01805,-8.973827 8.97462,-8.973827 z m 35.2316,37.450998 c -0.90048,29.80928 -23.66033,69.21262 -54.51292,79.34466 -36.04206,11.836 -63.40991,-5.92226 -72.08409,-26.74061 -6.75754,-16.21966 -1.65117,-35.62363 10.96266,-43.83669 10.6506,-6.93533 30.48543,-8.76736 47.15454,2.19144 -5.85627,-15.34246 -21.62491,-25.4256 -35.59101,-28.49424 -13.96613,-3.06867 -28.38324,0.43858 -38.74504,5.69946 13.29071,-14.68572 44.40801,-28.946049 78.24077,-10.95958 22.67676,12.05491 32.43775,28.93208 42.0489,51.72763 C 251.59637,117.87858 234.026,71.411066 203.39074,43.794029 172.15544,15.636686 129.95516,4.340214 97.668803,6.103155 108.32483,12.678273 120.84625,22.06586 132.41209,33.053363 81.298533,26.697169 39.174705,38.314245 13.119542,73.749217 27.67508,70.878527 46.868833,69.073666 65.974711,70.016861 28.737658,96.252107 7.1124298,140.38147 18.105298,186.43137 c 6.718497,-11.74129 16.767711,-25.84558 28.726275,-38.62863 -3.677175,34.36994 1.42836,80.83745 45.62293,110.85478 -2.25587,-9.42394 -4.08014,-20.88443 -4.91466,-33.0154 20.673197,16.1282 50.685067,29.42205 87.917917,20.24096 65.77679,-16.21975 83.34719,-79.78335 73.4356,-118.35996"
inkscape:pagecheckerboard="0" style="fill:#f36118;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0776283"
inkscape:deskcolor="#d1d1d1" id="path32" />
showgrid="false" </g>
inkscape:zoom="1.9583914" </svg>
inkscape:cx="127.40048"
inkscape:cy="262.71561"
inkscape:window-width="1436"
inkscape:window-height="1236"
inkscape:window-x="2208"
inkscape:window-y="511"
inkscape:window-maximized="0"
inkscape:current-layer="svg52" /><path
d="m 62.232921,184.91974 c 0,2.431 -1.97,4.402 -4.399,4.402 -2.429,0 -4.399,-1.972 -4.399,-4.402 0,-2.429 1.97,-4.399 4.399,-4.399 2.429,-10e-4 4.399,1.97 4.399,4.399 z m 58.993999,-4.821 c -25.943999,-2.826 -38.978999,7.453 -71.181999,31.357 -27.572,20.467 -32.767,4.381 -31.748,-2.614 1.499,-10.282 25.222,-58.573 48.079,-88.461 28.273,7.34 49.869999,30.727 54.850999,59.718 z m -55.915999,4.821 c 0,-4.131 -3.349,-7.478 -7.478,-7.478 -4.129,0 -7.478,3.347 -7.478,7.478 0,4.131 3.349,7.481 7.478,7.481 4.13,0 7.478,-3.35 7.478,-7.481 z m -15.032,48.424 -0.234,14.041 20.413,22.687 -9.818,7.353 33.306,27.492 -11.759,8.124 42.631999,19.939 -10.825,9.747 48.291,8.078 -7.526,10.307 48.758,-4.531 -3.997,11.725 53.916,-18.153 -2.76,13.357 48.077,-34.345 1.479,13.562 34.087,-48.576 7.478,14.206 15.187,-58.89 10.391,8.533 -2.14,-57.884 13.814,5.13 -21.082,-51.204 13.404,0.048 -33.696,-42.131 15.312,-1.366 -47.026,-32.831002 14.255,-8.399 -54.817,-14.682 9.257,-11.695 -49.625,0.352 0.6,-13.337 -38.537,14.084 -1.597,-12.689 -29.984,21.429 -6.446,-10.852 -22.59,26.504 -7.021,-9.572 -18.923,30.294 -9.595999,-8.744 -16.754,30.138002 c 31.509999,10.197 54.979999,37.951 59.126999,71.547 0.404,0.087 -22.37,31.257 10.955,57.85 -0.576,-2.985 -6.113,-53.902 47.496,-57.61 26.668,-1.844 48.4,21.666 48.4,48.399 0,8.184 -2.05,15.883 -5.636,22.64 -15.927,29.611 -64.858,30.755 -80.429,30.596 -45.154,-0.459 -104.051999,-51.521 -104.051999,-51.521 z"
id="path46"
style="fill:#f97315;fill-opacity:1" /></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -1,6 +1,6 @@
import { Request } from "express"; import { Request } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userActions, roleActions, userOrgs } from "@server/db/schemas"; import { userActions, roleActions, userOrgs } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -109,7 +109,7 @@ export async function checkUserActionPermission(
try { try {
let userRoleIds = req.userRoleIds; let userRoleIds = req.userRoleIds;
// If userOrgRoleId is not available on the request, fetch it // If userRoleIds is not available on the request, fetch it
if (userRoleIds === undefined) { if (userRoleIds === undefined) {
const userOrgRoles = await db const userOrgRoles = await db
.select({ roleId: userOrgs.roleId }) .select({ roleId: userOrgs.roleId })

View file

@ -1,6 +1,6 @@
import db from "@server/db"; import { db } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import { roleResources, userResources } from "@server/db/schemas"; import { roleResources, userResources } from "@server/db";
export async function canUserAccessResource({ export async function canUserAccessResource({
userId, userId,

View file

@ -1,5 +1,5 @@
import db from "@server/db"; import { db } from "@server/db";
import { UserInvite, userInvites } from "@server/db/schemas"; import { UserInvite, userInvites } from "@server/db";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "./password"; import { verifyPassword } from "./password";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View file

@ -1,5 +1,5 @@
import { db } from '@server/db'; import { db } from '@server/db';
import { limitsTable } from '@server/db/schemas'; import { limitsTable } from '@server/db';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode'; import HttpCode from '@server/types/HttpCode';

View file

@ -1,5 +1,5 @@
import db from "@server/db"; import { db } from "@server/db";
import { resourceOtp } from "@server/db/schemas"; import { resourceOtp } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { createDate, isWithinExpirationDate, TimeSpan } from "oslo"; import { createDate, isWithinExpirationDate, TimeSpan } from "oslo";
import { alphabet, generateRandomString, sha256 } from "oslo/crypto"; import { alphabet, generateRandomString, sha256 } from "oslo/crypto";

View file

@ -1,7 +1,7 @@
import { TimeSpan, createDate } from "oslo"; import { TimeSpan, createDate } from "oslo";
import { generateRandomString, alphabet } from "oslo/crypto"; import { generateRandomString, alphabet } from "oslo/crypto";
import db from "@server/db"; import { db } from "@server/db";
import { users, emailVerificationCodes } from "@server/db/schemas"; import { users, emailVerificationCodes } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { sendEmail } from "@server/emails"; import { sendEmail } from "@server/emails";
import config from "@server/lib/config"; import config from "@server/lib/config";

View file

@ -9,8 +9,8 @@ import {
sessions, sessions,
User, User,
users users
} from "@server/db/schemas"; } from "@server/db";
import db from "@server/db"; import { db } from "@server/db";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import type { RandomReader } from "@oslojs/crypto/random"; import type { RandomReader } from "@oslojs/crypto/random";

View file

@ -2,8 +2,8 @@ import {
encodeHexLowerCase, encodeHexLowerCase,
} from "@oslojs/encoding"; } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { Newt, newts, newtSessions, NewtSession } from "@server/db/schemas"; import { Newt, newts, newtSessions, NewtSession } from "@server/db";
import db from "@server/db"; import { db } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
export const EXPIRES = 1000 * 60 * 60 * 24 * 30; export const EXPIRES = 1000 * 60 * 60 * 24 * 30;

View file

@ -1,7 +1,7 @@
import { encodeHexLowerCase } from "@oslojs/encoding"; import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { resourceSessions, ResourceSession } from "@server/db/schemas"; import { resourceSessions, ResourceSession } from "@server/db";
import db from "@server/db"; import { db } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";

View file

@ -1,6 +1,6 @@
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import db from "@server/db"; import { db } from "@server/db";
import { twoFactorBackupCodes } from "@server/db/schemas"; import { twoFactorBackupCodes } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { decodeHex } from "oslo/encoding"; import { decodeHex } from "oslo/encoding";
import { TOTPController } from "oslo/otp"; import { TOTPController } from "oslo/otp";

View file

@ -1,10 +1,10 @@
import db from "@server/db"; import { db } from "@server/db";
import { import {
Resource, Resource,
ResourceAccessToken, ResourceAccessToken,
resourceAccessToken, resourceAccessToken,
resources resources
} from "@server/db/schemas"; } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "./password"; import { verifyPassword } from "./password";

72
server/db/README.md Normal file
View file

@ -0,0 +1,72 @@
# Database
Pangolin can use a Postgres or SQLite database to store its data.
## Development
### Postgres
To use Postgres, edit `server/db/index.ts` to export all from `server/db/pg/index.ts`:
```typescript
export * from "./pg";
```
Make sure you have a valid config file with a connection string:
```yaml
postgres:
connection_string: postgresql://postgres:postgres@localhost:5432
```
You can run an ephemeral Postgres database for local development using Docker:
```bash
docker run -d \
--name postgres \
--rm \
-p 5432:5432 \
-e POSTGRES_PASSWORD=postgres \
-v $(mktemp -d):/var/lib/postgresql/data \
postgres:17
```
### Schema
`server/db/pg/schema.ts` and `server/db/sqlite/schema.ts` contain the database schema definitions. These need to be kept in sync with with each other.
Stick to common data types and avoid Postgres-specific features to ensure compatibility with SQLite.
### SQLite
To use SQLite, edit `server/db/index.ts` to export all from `server/db/sqlite/index.ts`:
```typescript
export * from "./sqlite";
```
No edits to the config are needed. If you keep the Postgres config, it will be ignored.
## Generate and Push Migrations
Ensure drizzle-kit is installed.
### Postgres
You must have a connection string in your config file, as shown above.
```bash
npm run db:pg:generate
npm run db:pg:push
```
### SQLite
```bash
npm run db:sqlite:generate
npm run db:sqlite:push
```
## Build Time
There is a dockerfile for each database type. The dockerfile swaps out the `server/db/index.ts` file to use the correct database type.

View file

@ -1,52 +1,2 @@
import { drizzle } from "drizzle-orm/better-sqlite3"; export * from "./sqlite";
import Database from "better-sqlite3"; // export * from "./pg";
import * as schema from "@server/db/schemas";
import path from "path";
import fs from "fs/promises";
import { APP_PATH } from "@server/lib/consts";
import { existsSync, mkdirSync } from "fs";
export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = await checkFileExists(location);
bootstrapVolume();
const sqlite = new Database(location);
export const db = drizzle(sqlite, { schema });
export default db;
async function checkFileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function bootstrapVolume() {
const appPath = APP_PATH;
const dbDir = path.join(appPath, "db");
const logsDir = path.join(appPath, "logs");
// check if the db directory exists and create it if it doesn't
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true });
}
// check if the logs directory exists and create it if it doesn't
if (!existsSync(logsDir)) {
mkdirSync(logsDir, { recursive: true });
}
// THIS IS FOR TRAEFIK; NOT REALLY NEEDED, BUT JUST IN CASE
const traefikDir = path.join(appPath, "traefik");
// check if the traefik directory exists and create it if it doesn't
if (!existsSync(traefikDir)) {
mkdirSync(traefikDir, { recursive: true });
}
}

View file

@ -1,7 +1,7 @@
import { join } from "path"; import { join } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { db } from "@server/db"; import { db } from "@server/db";
import { exitNodes, sites } from "./schemas/schema"; import { exitNodes, sites } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { __DIRNAME } from "@server/lib/consts"; import { __DIRNAME } from "@server/lib/consts";

17
server/db/pg/driver.ts Normal file
View file

@ -0,0 +1,17 @@
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { readConfigFile } from "@server/lib/readConfigFile";
function createDb() {
const config = readConfigFile();
const connectionString = config.postgres?.connection_string;
if (!connectionString) {
throw new Error("Postgres connection string is not defined in the configuration file.");
}
return DrizzlePostgres(connectionString);
}
export const db = createDb();
export default db;

View file

@ -1 +1,2 @@
export * from "./driver";
export * from "./schema"; export * from "./schema";

20
server/db/pg/migrate.ts Normal file
View file

@ -0,0 +1,20 @@
import { migrate } from "drizzle-orm/node-postgres/migrator";
import db from "./driver";
import path from "path";
const migrationsFolder = path.join("server/migrations");
const runMigrations = async () => {
console.log("Running migrations...");
try {
await migrate(db as any, {
migrationsFolder: migrationsFolder
});
console.log("Migrations completed successfully.");
} catch (error) {
console.error("Error running migrations:", error);
process.exit(1);
}
};
runMigrations();

532
server/db/pg/schema.ts Normal file
View file

@ -0,0 +1,532 @@
import {
pgTable,
serial,
varchar,
boolean,
integer,
bigint,
real
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(),
baseDomain: varchar("baseDomain").notNull(),
configManaged: boolean("configManaged").notNull().default(false)
});
export const orgs = pgTable("orgs", {
orgId: varchar("orgId").primaryKey(),
name: varchar("name").notNull()
});
export const orgDomains = pgTable("orgDomains", {
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
domainId: varchar("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" })
});
export const sites = pgTable("sites", {
siteId: serial("siteId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
niceId: varchar("niceId").notNull(),
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
onDelete: "set null"
}),
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(),
megabytesIn: real("bytesIn"),
megabytesOut: real("bytesOut"),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
type: varchar("type").notNull(), // "newt" or "wireguard"
online: boolean("online").notNull().default(false),
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
});
export const resources = pgTable("resources", {
resourceId: serial("resourceId").primaryKey(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
name: varchar("name").notNull(),
subdomain: varchar("subdomain"),
fullDomain: varchar("fullDomain"),
domainId: varchar("domainId").references(() => domains.domainId, {
onDelete: "set null"
}),
ssl: boolean("ssl").notNull().default(false),
blockAccess: boolean("blockAccess").notNull().default(false),
sso: boolean("sso").notNull().default(true),
http: boolean("http").notNull().default(true),
protocol: varchar("protocol").notNull(),
proxyPort: integer("proxyPort"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull()
.default(false),
isBaseDomain: boolean("isBaseDomain"),
applyRules: boolean("applyRules").notNull().default(false),
enabled: boolean("enabled").notNull().default(true),
stickySession: boolean("stickySession").notNull().default(false),
tlsServerName: varchar("tlsServerName"),
setHostHeader: varchar("setHostHeader")
});
export const targets = pgTable("targets", {
targetId: serial("targetId").primaryKey(),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
onDelete: "cascade"
})
.notNull(),
ip: varchar("ip").notNull(),
method: varchar("method"),
port: integer("port").notNull(),
internalPort: integer("internalPort"),
enabled: boolean("enabled").notNull().default(true)
});
export const exitNodes = pgTable("exitNodes", {
exitNodeId: serial("exitNodeId").primaryKey(),
name: varchar("name").notNull(),
address: varchar("address").notNull(),
endpoint: varchar("endpoint").notNull(),
publicKey: varchar("publicKey").notNull(),
listenPort: integer("listenPort").notNull(),
reachableAt: varchar("reachableAt")
});
export const users = pgTable("user", {
userId: varchar("id").primaryKey(),
email: varchar("email"),
username: varchar("username").notNull(),
name: varchar("name"),
type: varchar("type").notNull(), // "internal", "oidc"
idpId: integer("idpId").references(() => idp.idpId, {
onDelete: "cascade"
}),
passwordHash: varchar("passwordHash"),
twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false),
twoFactorSecret: varchar("twoFactorSecret"),
emailVerified: boolean("emailVerified").notNull().default(false),
dateCreated: varchar("dateCreated").notNull(),
serverAdmin: boolean("serverAdmin").notNull().default(false)
});
export const newts = pgTable("newt", {
newtId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
})
});
export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
codeId: serial("id").primaryKey(),
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
codeHash: varchar("codeHash").notNull()
});
export const sessions = pgTable("session", {
sessionId: varchar("id").primaryKey(),
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const newtSessions = pgTable("newtSession", {
sessionId: varchar("id").primaryKey(),
newtId: varchar("newtId")
.notNull()
.references(() => newts.newtId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const userOrgs = pgTable("userOrgs", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId),
isOwner: boolean("isOwner").notNull().default(false)
});
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
codeId: serial("id").primaryKey(),
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
email: varchar("email").notNull(),
code: varchar("code").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const passwordResetTokens = pgTable("passwordResetTokens", {
tokenId: serial("id").primaryKey(),
email: varchar("email").notNull(),
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
tokenHash: varchar("tokenHash").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const actions = pgTable("actions", {
actionId: varchar("actionId").primaryKey(),
name: varchar("name"),
description: varchar("description")
});
export const roles = pgTable("roles", {
roleId: serial("roleId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
isAdmin: boolean("isAdmin"),
name: varchar("name").notNull(),
description: varchar("description")
});
export const roleActions = pgTable("roleActions", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
actionId: varchar("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
});
export const userActions = pgTable("userActions", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
actionId: varchar("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
});
export const roleSites = pgTable("roleSites", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" })
});
export const userSites = pgTable("userSites", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" })
});
export const roleResources = pgTable("roleResources", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
});
export const userResources = pgTable("userResources", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
});
export const limitsTable = pgTable("limits", {
limitId: serial("limitId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
name: varchar("name").notNull(),
value: bigint("value", { mode: "number" }).notNull(),
description: varchar("description")
});
export const userInvites = pgTable("userInvites", {
inviteId: varchar("inviteId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
email: varchar("email").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
tokenHash: varchar("token").notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
});
export const resourcePincode = pgTable("resourcePincode", {
pincodeId: serial("pincodeId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
pincodeHash: varchar("pincodeHash").notNull(),
digitLength: integer("digitLength").notNull()
});
export const resourcePassword = pgTable("resourcePassword", {
passwordId: serial("passwordId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
passwordHash: varchar("passwordHash").notNull()
});
export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
tokenHash: varchar("tokenHash").notNull(),
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }),
title: varchar("title"),
description: varchar("description"),
createdAt: bigint("createdAt", { mode: "number" }).notNull()
});
export const resourceSessions = pgTable("resourceSessions", {
sessionId: varchar("id").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
doNotExtend: boolean("doNotExtend").notNull().default(false),
isRequestToken: boolean("isRequestToken"),
userSessionId: varchar("userSessionId").references(
() => sessions.sessionId,
{
onDelete: "cascade"
}
),
passwordId: integer("passwordId").references(
() => resourcePassword.passwordId,
{
onDelete: "cascade"
}
),
pincodeId: integer("pincodeId").references(
() => resourcePincode.pincodeId,
{
onDelete: "cascade"
}
),
whitelistId: integer("whitelistId").references(
() => resourceWhitelist.whitelistId,
{
onDelete: "cascade"
}
),
accessTokenId: varchar("accessTokenId").references(
() => resourceAccessToken.accessTokenId,
{
onDelete: "cascade"
}
)
});
export const resourceWhitelist = pgTable("resourceWhitelist", {
whitelistId: serial("id").primaryKey(),
email: varchar("email").notNull(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
});
export const resourceOtp = pgTable("resourceOtp", {
otpId: serial("otpId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
email: varchar("email").notNull(),
otpHash: varchar("otpHash").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const versionMigrations = pgTable("versionMigrations", {
version: varchar("version").primaryKey(),
executedAt: bigint("executedAt", { mode: "number" }).notNull()
});
export const resourceRules = pgTable("resourceRules", {
ruleId: serial("ruleId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(),
action: varchar("action").notNull(), // ACCEPT, DROP
match: varchar("match").notNull(), // CIDR, PATH, IP
value: varchar("value").notNull()
});
export const supporterKey = pgTable("supporterKey", {
keyId: serial("keyId").primaryKey(),
key: varchar("key").notNull(),
githubUsername: varchar("githubUsername").notNull(),
phrase: varchar("phrase"),
tier: varchar("tier"),
valid: boolean("valid").notNull().default(false)
});
export const idp = pgTable("idp", {
idpId: serial("idpId").primaryKey(),
name: varchar("name").notNull(),
type: varchar("type").notNull(),
defaultRoleMapping: varchar("defaultRoleMapping"),
defaultOrgMapping: varchar("defaultOrgMapping"),
autoProvision: boolean("autoProvision").notNull().default(false)
});
export const idpOidcConfig = pgTable("idpOidcConfig", {
idpOauthConfigId: serial("idpOauthConfigId").primaryKey(),
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
clientId: varchar("clientId").notNull(),
clientSecret: varchar("clientSecret").notNull(),
authUrl: varchar("authUrl").notNull(),
tokenUrl: varchar("tokenUrl").notNull(),
identifierPath: varchar("identifierPath").notNull(),
emailPath: varchar("emailPath"),
namePath: varchar("namePath"),
scopes: varchar("scopes").notNull()
});
export const licenseKey = pgTable("licenseKey", {
licenseKeyId: varchar("licenseKeyId").primaryKey().notNull(),
instanceId: varchar("instanceId").notNull(),
token: varchar("token").notNull()
});
export const hostMeta = pgTable("hostMeta", {
hostMetaId: varchar("hostMetaId").primaryKey().notNull(),
createdAt: bigint("createdAt", { mode: "number" }).notNull()
});
export const apiKeys = pgTable("apiKeys", {
apiKeyId: varchar("apiKeyId").primaryKey(),
name: varchar("name").notNull(),
apiKeyHash: varchar("apiKeyHash").notNull(),
lastChars: varchar("lastChars").notNull(),
createdAt: varchar("dateCreated").notNull(),
isRoot: boolean("isRoot").notNull().default(false)
});
export const apiKeyActions = pgTable("apiKeyActions", {
apiKeyId: varchar("apiKeyId")
.notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
actionId: varchar("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" })
});
export const apiKeyOrg = pgTable("apiKeyOrg", {
apiKeyId: varchar("apiKeyId")
.notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const idpOrg = pgTable("idpOrg", {
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
roleMapping: varchar("roleMapping"),
orgMapping: varchar("orgMapping")
});
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
export type Resource = InferSelectModel<typeof resources>;
export type ExitNode = InferSelectModel<typeof exitNodes>;
export type Target = InferSelectModel<typeof targets>;
export type Session = InferSelectModel<typeof sessions>;
export type Newt = InferSelectModel<typeof newts>;
export type NewtSession = InferSelectModel<typeof newtSessions>;
export type EmailVerificationCode = InferSelectModel<
typeof emailVerificationCodes
>;
export type TwoFactorBackupCode = InferSelectModel<typeof twoFactorBackupCodes>;
export type PasswordResetToken = InferSelectModel<typeof passwordResetTokens>;
export type Role = InferSelectModel<typeof roles>;
export type Action = InferSelectModel<typeof actions>;
export type RoleAction = InferSelectModel<typeof roleActions>;
export type UserAction = InferSelectModel<typeof userActions>;
export type RoleSite = InferSelectModel<typeof roleSites>;
export type UserSite = InferSelectModel<typeof userSites>;
export type RoleResource = InferSelectModel<typeof roleResources>;
export type UserResource = InferSelectModel<typeof userResources>;
export type Limit = InferSelectModel<typeof limitsTable>;
export type UserInvite = InferSelectModel<typeof userInvites>;
export type UserOrg = InferSelectModel<typeof userOrgs>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
export type ResourceRule = InferSelectModel<typeof resourceRules>;
export type Domain = InferSelectModel<typeof domains>;
export type SupporterKey = InferSelectModel<typeof supporterKey>;
export type Idp = InferSelectModel<typeof idp>;
export type ApiKey = InferSelectModel<typeof apiKeys>;
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;

View file

@ -0,0 +1,58 @@
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import * as schema from "./schema";
import path from "path";
import fs from "fs/promises";
import { APP_PATH } from "@server/lib/consts";
import { existsSync, mkdirSync } from "fs";
import { readConfigFile } from "@server/lib/readConfigFile";
export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = await checkFileExists(location);
bootstrapVolume();
function createDb() {
const config = readConfigFile();
const sqlite = new Database(location);
return DrizzleSqlite(sqlite, { schema });
}
export const db = createDb();
export default db;
async function checkFileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function bootstrapVolume() {
const appPath = APP_PATH;
const dbDir = path.join(appPath, "db");
const logsDir = path.join(appPath, "logs");
// check if the db directory exists and create it if it doesn't
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true });
}
// check if the logs directory exists and create it if it doesn't
if (!existsSync(logsDir)) {
mkdirSync(logsDir, { recursive: true });
}
// THIS IS FOR TRAEFIK; NOT REALLY NEEDED, BUT JUST IN CASE
const traefikDir = path.join(appPath, "traefik");
// check if the traefik directory exists and create it if it doesn't
if (!existsSync(traefikDir)) {
mkdirSync(traefikDir, { recursive: true });
}
}

View file

@ -0,0 +1,2 @@
export * from "./driver";
export * from "./schema";

View file

@ -1,5 +1,5 @@
import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import db from "@server/db"; import db from "./driver";
import path from "path"; import path from "path";
const migrationsFolder = path.join("server/migrations"); const migrationsFolder = path.join("server/migrations");
@ -7,7 +7,7 @@ const migrationsFolder = path.join("server/migrations");
const runMigrations = async () => { const runMigrations = async () => {
console.log("Running migrations..."); console.log("Running migrations...");
try { try {
migrate(db, { migrate(db as any, {
migrationsFolder: migrationsFolder, migrationsFolder: migrationsFolder,
}); });
console.log("Migrations completed successfully."); console.log("Migrations completed successfully.");

View file

@ -41,7 +41,10 @@ export const sites = sqliteTable("sites", {
megabytesOut: integer("bytesOut"), megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: text("lastBandwidthUpdate"), lastBandwidthUpdate: text("lastBandwidthUpdate"),
type: text("type").notNull(), // "newt" or "wireguard" type: text("type").notNull(), // "newt" or "wireguard"
online: integer("online", { mode: "boolean" }).notNull().default(false) online: integer("online", { mode: "boolean" }).notNull().default(false),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
.notNull()
.default(true)
}); });
export const resources = sqliteTable("resources", { export const resources = sqliteTable("resources", {

View file

@ -4,8 +4,9 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer"; import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer"; import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
// import { createIntegrationApiServer } from "./integrationApiServer"; import { createIntegrationApiServer } from "./integrationApiServer";
import config from "@server/lib/config";
async function startServers() { async function startServers() {
await runSetupFunctions(); await runSetupFunctions();
@ -16,7 +17,9 @@ async function startServers() {
const nextServer = await createNextServer(); const nextServer = await createNextServer();
let integrationServer; let integrationServer;
// integrationServer = createIntegrationApiServer(); if (config.getRawConfig().flags?.enable_integration_api) {
integrationServer = createIntegrationApiServer();
}
return { return {
apiServer, apiServer,

View file

@ -0,0 +1,102 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import config from "@server/lib/config";
import logger from "@server/logger";
import {
errorHandlerMiddleware,
notFoundMiddleware,
} from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/integration";
import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet";
import swaggerUi from "swagger-ui-express";
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import { registry } from "./openApi";
const dev = process.env.ENVIRONMENT !== "prod";
const externalPort = config.getRawConfig().server.integration_port;
export function createIntegrationApiServer() {
const apiServer = express();
if (config.getRawConfig().server.trust_proxy) {
apiServer.set("trust proxy", 1);
}
apiServer.use(cors());
if (!dev) {
apiServer.use(helmet());
}
apiServer.use(cookieParser());
apiServer.use(express.json());
apiServer.use(
"/v1/docs",
swaggerUi.serve,
swaggerUi.setup(getOpenApiDocumentation())
);
// API routes
const prefix = `/v1`;
apiServer.use(logIncomingMiddleware);
apiServer.use(prefix, unauthenticated);
apiServer.use(prefix, authenticated);
// Error handling
apiServer.use(notFoundMiddleware);
apiServer.use(errorHandlerMiddleware);
// Create HTTP server
const httpServer = apiServer.listen(externalPort, (err?: any) => {
if (err) throw err;
logger.info(
`Integration API server is running on http://localhost:${externalPort}`
);
});
return httpServer;
}
function getOpenApiDocumentation() {
const bearerAuth = registry.registerComponent(
"securitySchemes",
"Bearer Auth",
{
type: "http",
scheme: "bearer"
}
);
for (const def of registry.definitions) {
if (def.type === "route") {
def.route.security = [
{
[bearerAuth.name]: []
}
];
}
}
registry.registerPath({
method: "get",
path: "/",
description: "Health check",
tags: [],
request: {},
responses: {}
});
const generator = new OpenApiGeneratorV3(registry.definitions);
return generator.generateDocument({
openapi: "3.0.0",
info: {
version: "v1",
title: "Pangolin Integration API"
},
servers: [{ url: "/v1" }]
});
}

View file

@ -1,6 +1,6 @@
import db from "@server/db"; import { db } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import { roleResources, userResources } from "@server/db/schemas"; import { roleResources, userResources } from "@server/db";
export async function canUserAccessResource({ export async function canUserAccessResource({
userId, userId,

View file

@ -1,169 +1,6 @@
import fs from "fs";
import yaml from "js-yaml";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
import { import { configSchema, readConfigFile } from "./readConfigFile";
__DIRNAME,
APP_VERSION,
configFilePath1,
configFilePath2
} from "@server/lib/consts";
import { passwordSchema } from "@server/auth/passwordSchema";
import stoi from "./stoi";
const portSchema = z.number().positive().gt(0).lte(65535);
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
return process.env[envVar] ?? valFromYaml;
};
const configSchema = z.object({
app: z.object({
dashboard_url: z
.string()
.url()
.optional()
.pipe(z.string().url())
.transform((url) => url.toLowerCase()),
log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean(),
log_failed_attempts: z.boolean().optional()
}),
domains: z
.record(
z.string(),
z.object({
base_domain: z
.string()
.nonempty("base_domain must not be empty")
.transform((url) => url.toLowerCase()),
cert_resolver: z.string().optional(),
prefer_wildcard_cert: z.boolean().optional()
})
)
.refine(
(domains) => {
const keys = Object.keys(domains);
if (keys.length === 0) {
return false;
}
return true;
},
{
message: "At least one domain must be defined"
}
),
server: z.object({
integration_port: portSchema
.optional()
.transform(stoi)
.pipe(portSchema.optional()),
external_port: portSchema.optional().transform(stoi).pipe(portSchema),
internal_port: portSchema.optional().transform(stoi).pipe(portSchema),
next_port: portSchema.optional().transform(stoi).pipe(portSchema),
internal_hostname: z.string().transform((url) => url.toLowerCase()),
session_cookie_name: z.string(),
resource_access_token_param: z.string(),
resource_access_token_headers: z.object({
id: z.string(),
token: z.string()
}),
resource_session_request_param: z.string(),
dashboard_session_length_hours: z
.number()
.positive()
.gt(0)
.optional()
.default(720),
resource_session_length_hours: z
.number()
.positive()
.gt(0)
.optional()
.default(720),
cors: z
.object({
origins: z.array(z.string()).optional(),
methods: z.array(z.string()).optional(),
allowed_headers: z.array(z.string()).optional(),
credentials: z.boolean().optional()
})
.optional(),
trust_proxy: z.boolean().optional().default(true),
secret: z
.string()
.optional()
.transform(getEnvOrYaml("SERVER_SECRET"))
.pipe(z.string().min(8))
}),
traefik: z.object({
http_entrypoint: z.string(),
https_entrypoint: z.string().optional(),
additional_middlewares: z.array(z.string()).optional()
}),
gerbil: z.object({
start_port: portSchema.optional().transform(stoi).pipe(portSchema),
base_endpoint: z
.string()
.optional()
.pipe(z.string())
.transform((url) => url.toLowerCase()),
use_subdomain: z.boolean(),
subnet_group: z.string(),
block_size: z.number().positive().gt(0),
site_block_size: z.number().positive().gt(0)
}),
rate_limits: z.object({
global: z.object({
window_minutes: z.number().positive().gt(0),
max_requests: z.number().positive().gt(0)
}),
auth: z
.object({
window_minutes: z.number().positive().gt(0),
max_requests: z.number().positive().gt(0)
})
.optional()
}),
email: z
.object({
smtp_host: z.string().optional(),
smtp_port: portSchema.optional(),
smtp_user: z.string().optional(),
smtp_pass: z.string().optional(),
smtp_secure: z.boolean().optional(),
smtp_tls_reject_unauthorized: z.boolean().optional(),
no_reply: z.string().email().optional()
})
.optional(),
users: z.object({
server_admin: z.object({
email: z
.string()
.email()
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
.pipe(z.string().email())
.transform((v) => v.toLowerCase()),
password: passwordSchema
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
.pipe(passwordSchema)
})
}),
flags: z
.object({
require_email_verification: z.boolean().optional(),
disable_signup_without_invite: z.boolean().optional(),
disable_user_create_org: z.boolean().optional(),
allow_raw_resources: z.boolean().optional(),
allow_base_domain_resources: z.boolean().optional(),
allow_local_sites: z.boolean().optional()
})
.optional()
});
export class Config { export class Config {
private rawConfig!: z.infer<typeof configSchema>; private rawConfig!: z.infer<typeof configSchema>;
@ -171,92 +8,53 @@ export class Config {
isDev: boolean = process.env.ENVIRONMENT !== "prod"; isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() { constructor() {
this.loadConfig(); this.load();
} }
public loadConfig() { public load() {
const loadConfig = (configPath: string) => { const parsedConfig = readConfigFile();
try {
const yamlContent = fs.readFileSync(configPath, "utf8");
const config = yaml.load(yamlContent);
return config;
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Error loading configuration file: ${error.message}`
);
}
throw error;
}
};
let environment: any;
if (fs.existsSync(configFilePath1)) {
environment = loadConfig(configFilePath1);
} else if (fs.existsSync(configFilePath2)) {
environment = loadConfig(configFilePath2);
}
if (process.env.APP_BASE_DOMAIN) {
console.log(
"You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
);
}
if (!environment) {
throw new Error(
"No configuration file found. Please create one. https://docs.fossorial.io/"
);
}
const parsedConfig = configSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid configuration file: ${errors}`);
}
process.env.APP_VERSION = APP_VERSION; process.env.APP_VERSION = APP_VERSION;
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString(); process.env.NEXT_PORT = parsedConfig.server.next_port.toString();
process.env.SERVER_EXTERNAL_PORT = process.env.SERVER_EXTERNAL_PORT =
parsedConfig.data.server.external_port.toString(); parsedConfig.server.external_port.toString();
process.env.SERVER_INTERNAL_PORT = process.env.SERVER_INTERNAL_PORT =
parsedConfig.data.server.internal_port.toString(); parsedConfig.server.internal_port.toString();
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.flags
?.require_email_verification ?.require_email_verification
? "true" ? "true"
: "false"; : "false";
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.flags
?.allow_raw_resources ?.allow_raw_resources
? "true" ? "true"
: "false"; : "false";
process.env.SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME =
parsedConfig.data.server.session_cookie_name; parsedConfig.server.session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false"; process.env.EMAIL_ENABLED = parsedConfig.email ? "true" : "false";
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.flags
?.disable_signup_without_invite ?.disable_signup_without_invite
? "true" ? "true"
: "false"; : "false";
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags process.env.DISABLE_USER_CREATE_ORG = parsedConfig.flags
?.disable_user_create_org ?.disable_user_create_org
? "true" ? "true"
: "false"; : "false";
process.env.RESOURCE_ACCESS_TOKEN_PARAM = process.env.RESOURCE_ACCESS_TOKEN_PARAM =
parsedConfig.data.server.resource_access_token_param; parsedConfig.server.resource_access_token_param;
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID = process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID =
parsedConfig.data.server.resource_access_token_headers.id; parsedConfig.server.resource_access_token_headers.id;
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN = process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN =
parsedConfig.data.server.resource_access_token_headers.token; parsedConfig.server.resource_access_token_headers.token;
process.env.RESOURCE_SESSION_REQUEST_PARAM = process.env.RESOURCE_SESSION_REQUEST_PARAM =
parsedConfig.data.server.resource_session_request_param; parsedConfig.server.resource_session_request_param;
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.flags
?.allow_base_domain_resources ?.allow_base_domain_resources
? "true" ? "true"
: "false"; : "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
this.rawConfig = parsedConfig.data; this.rawConfig = parsedConfig;
} }
public getRawConfig() { public getRawConfig() {

View file

@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.3.0"; export const APP_VERSION = "1.5.1";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View file

@ -0,0 +1,264 @@
import fs from "fs";
import yaml from "js-yaml";
import { configFilePath1, configFilePath2 } from "./consts";
import { z } from "zod";
import stoi from "./stoi";
import { passwordSchema } from "@server/auth/passwordSchema";
import { fromError } from "zod-validation-error";
const portSchema = z.number().positive().gt(0).lte(65535);
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
return process.env[envVar] ?? valFromYaml;
};
export const configSchema = z.object({
app: z.object({
dashboard_url: z
.string()
.url()
.optional()
.pipe(z.string().url())
.transform((url) => url.toLowerCase()),
log_level: z
.enum(["debug", "info", "warn", "error"])
.optional()
.default("info"),
save_logs: z.boolean().optional().default(false),
log_failed_attempts: z.boolean().optional().default(false)
}),
domains: z
.record(
z.string(),
z.object({
base_domain: z
.string()
.nonempty("base_domain must not be empty")
.transform((url) => url.toLowerCase()),
cert_resolver: z.string().optional().default("letsencrypt"),
prefer_wildcard_cert: z.boolean().optional().default(false)
})
)
.refine(
(domains) => {
const keys = Object.keys(domains);
if (keys.length === 0) {
return false;
}
return true;
},
{
message: "At least one domain must be defined"
}
),
server: z.object({
integration_port: portSchema
.optional()
.default(3003)
.transform(stoi)
.pipe(portSchema.optional()),
external_port: portSchema
.optional()
.default(3000)
.transform(stoi)
.pipe(portSchema),
internal_port: portSchema
.optional()
.default(3001)
.transform(stoi)
.pipe(portSchema),
next_port: portSchema
.optional()
.default(3002)
.transform(stoi)
.pipe(portSchema),
internal_hostname: z
.string()
.optional()
.default("pangolin")
.transform((url) => url.toLowerCase()),
session_cookie_name: z.string().optional().default("p_session_token"),
resource_access_token_param: z.string().optional().default("p_token"),
resource_access_token_headers: z
.object({
id: z.string().optional().default("P-Access-Token-Id"),
token: z.string().optional().default("P-Access-Token")
})
.optional()
.default({}),
resource_session_request_param: z
.string()
.optional()
.default("resource_session_request_param"),
dashboard_session_length_hours: z
.number()
.positive()
.gt(0)
.optional()
.default(720),
resource_session_length_hours: z
.number()
.positive()
.gt(0)
.optional()
.default(720),
cors: z
.object({
origins: z.array(z.string()).optional(),
methods: z.array(z.string()).optional(),
allowed_headers: z.array(z.string()).optional(),
credentials: z.boolean().optional()
})
.optional(),
trust_proxy: z.boolean().optional().default(true),
secret: z
.string()
.optional()
.transform(getEnvOrYaml("SERVER_SECRET"))
.pipe(z.string().min(8))
}),
postgres: z
.object({
connection_string: z.string().optional()
})
.default({}),
traefik: z
.object({
http_entrypoint: z.string().optional().default("web"),
https_entrypoint: z.string().optional().default("websecure"),
additional_middlewares: z.array(z.string()).optional()
})
.optional()
.default({}),
gerbil: z
.object({
start_port: portSchema
.optional()
.default(51820)
.transform(stoi)
.pipe(portSchema),
base_endpoint: z
.string()
.optional()
.pipe(z.string())
.transform((url) => url.toLowerCase()),
use_subdomain: z.boolean().optional().default(false),
subnet_group: z.string().optional().default("100.89.137.0/20"),
block_size: z.number().positive().gt(0).optional().default(24),
site_block_size: z.number().positive().gt(0).optional().default(30)
})
.optional()
.default({}),
rate_limits: z
.object({
global: z
.object({
window_minutes: z
.number()
.positive()
.gt(0)
.optional()
.default(1),
max_requests: z
.number()
.positive()
.gt(0)
.optional()
.default(500)
})
.optional()
.default({}),
auth: z
.object({
window_minutes: z.number().positive().gt(0),
max_requests: z.number().positive().gt(0)
})
.optional()
})
.optional()
.default({}),
email: z
.object({
smtp_host: z.string().optional(),
smtp_port: portSchema.optional(),
smtp_user: z.string().optional(),
smtp_pass: z.string().optional(),
smtp_secure: z.boolean().optional(),
smtp_tls_reject_unauthorized: z.boolean().optional(),
no_reply: z.string().email().optional()
})
.optional(),
users: z.object({
server_admin: z.object({
email: z
.string()
.email()
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
.pipe(z.string().email())
.transform((v) => v.toLowerCase()),
password: passwordSchema
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
.pipe(passwordSchema)
})
}),
flags: z
.object({
require_email_verification: z.boolean().optional(),
disable_signup_without_invite: z.boolean().optional(),
disable_user_create_org: z.boolean().optional(),
allow_raw_resources: z.boolean().optional(),
allow_base_domain_resources: z.boolean().optional(),
allow_local_sites: z.boolean().optional(),
enable_integration_api: z.boolean().optional()
})
.optional()
});
export function readConfigFile() {
const loadConfig = (configPath: string) => {
try {
const yamlContent = fs.readFileSync(configPath, "utf8");
const config = yaml.load(yamlContent);
return config;
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Error loading configuration file: ${error.message}`
);
}
throw error;
}
};
let environment: any;
if (fs.existsSync(configFilePath1)) {
environment = loadConfig(configFilePath1);
} else if (fs.existsSync(configFilePath2)) {
environment = loadConfig(configFilePath2);
}
if (process.env.APP_BASE_DOMAIN) {
console.log(
"You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
);
}
if (!environment) {
throw new Error(
"No configuration file found. Please create one. https://docs.fossorial.io/"
);
}
const parsedConfig = configSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid configuration file: ${errors}`);
}
return parsedConfig.data;
}

View file

@ -9,6 +9,10 @@ export function isValidIP(ip: string): boolean {
} }
export function isValidUrlGlobPattern(pattern: string): boolean { export function isValidUrlGlobPattern(pattern: string): boolean {
if (pattern === "/") {
return true;
}
// Remove leading slash if present // Remove leading slash if present
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;

View file

@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db, userOrgs } from "@server/db";
import { userOrgs, orgs } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -16,6 +16,6 @@ export * from "./verifyUserInRole";
export * from "./verifyAccessTokenAccess"; export * from "./verifyAccessTokenAccess";
export * from "./verifyUserIsServerAdmin"; export * from "./verifyUserIsServerAdmin";
export * from "./verifyIsLoggedInUser"; export * from "./verifyIsLoggedInUser";
// export * from "./integration"; export * from "./integration";
export * from "./verifyUserHasAction"; export * from "./verifyUserHasAction";
// export * from "./verifyApiKeyAccess"; export * from "./verifyApiKeyAccess";

View file

@ -0,0 +1,12 @@
export * from "./verifyApiKey";
export * from "./verifyApiKeyOrgAccess";
export * from "./verifyApiKeyHasAction";
export * from "./verifyApiKeySiteAccess";
export * from "./verifyApiKeyResourceAccess";
export * from "./verifyApiKeyTargetAccess";
export * from "./verifyApiKeyRoleAccess";
export * from "./verifyApiKeyUserAccess";
export * from "./verifyApiKeySetResourceUsers";
export * from "./verifyAccessTokenAccess";
export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess";

View file

@ -0,0 +1,110 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { resourceAccessToken, resources, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyAccessTokenAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const accessTokenId = req.params.accessTokenId;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
const [accessToken] = await db
.select()
.from(resourceAccessToken)
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
.limit(1);
if (!accessToken) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Access token with ID ${accessTokenId} not found`
)
);
}
const resourceId = accessToken.resourceId;
if (!resourceId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Access token with ID ${accessTokenId} does not have a resource ID`
)
);
}
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`
)
);
}
if (!resource.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Resource with ID ${resourceId} does not have an organization ID`
)
);
}
// Verify that the API key is linked to the resource's organization
if (!req.apiKeyOrg) {
const apiKeyOrgResult = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, resource.orgId)
)
)
.limit(1);
if (apiKeyOrgResult.length > 0) {
req.apiKeyOrg = apiKeyOrgResult[0];
}
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying access token access"
)
);
}
}

View file

@ -0,0 +1,60 @@
import { verifyPassword } from "@server/auth/password";
import { db } from "@server/db";
import { apiKeys } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { eq } from "drizzle-orm";
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
export async function verifyApiKey(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const authHeader = req.headers["authorization"];
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "API key required")
);
}
const key = authHeader.split(" ")[1]; // Get the token part after "Bearer"
const [apiKeyId, apiKeySecret] = key.split(".");
const [apiKey] = await db
.select()
.from(apiKeys)
.where(eq(apiKeys.apiKeyId, apiKeyId))
.limit(1);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key")
);
}
const secretHash = apiKey.apiKeyHash;
const valid = await verifyPassword(apiKeySecret, secretHash);
if (!valid) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key")
);
}
req.apiKey = apiKey;
return next();
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred checking API key"
)
);
}
}

View file

@ -0,0 +1,81 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { apiKeys, apiKeyOrg } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyApiKeyAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const {apiKey: callerApiKey } = req;
const apiKeyId =
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
const orgId = req.params.orgId;
if (!callerApiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!apiKeyId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);
}
const [callerApiKeyOrg] = await db
.select()
.from(apiKeyOrg)
.where(
and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId))
)
.limit(1);
if (!callerApiKeyOrg) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`API key with ID ${apiKeyId} does not have an organization ID`
)
);
}
const [otherApiKeyOrg] = await db
.select()
.from(apiKeyOrg)
.where(
and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId))
)
.limit(1);
if (!otherApiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`API key with ID ${apiKeyId} does not have access to organization with ID ${orgId}`
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying key access"
)
);
}
}

View file

@ -0,0 +1,56 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { ActionsEnum } from "@server/auth/actions";
import { db } from "@server/db";
import { apiKeyActions } from "@server/db";
import { and, eq } from "drizzle-orm";
export function verifyApiKeyHasAction(action: ActionsEnum) {
return async function (
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
if (!req.apiKey) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"API Key not authenticated"
)
);
}
const [actionRes] = await db
.select()
.from(apiKeyActions)
.where(
and(
eq(apiKeyActions.apiKeyId, req.apiKey.apiKeyId),
eq(apiKeyActions.actionId, action)
)
);
if (!actionRes) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have permission perform this action"
)
);
}
return next();
} catch (error) {
logger.error("Error verifying key action access:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying key action access"
)
);
}
};
}

View file

@ -0,0 +1,39 @@
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
export async function verifyApiKeyIsRoot(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { apiKey } = req;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!apiKey.isRoot) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have root access"
)
);
}
return next();
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred checking API key"
)
);
}
}

View file

@ -0,0 +1,61 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
export async function verifyApiKeyOrgAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKeyId = req.apiKey?.apiKeyId;
const orgId = req.params.orgId;
if (!apiKeyId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKeyId),
eq(apiKeyOrg.orgId, orgId)
)
);
req.apiKeyOrg = apiKeyOrgRes[0];
}
if (!req.apiKeyOrg) {
next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying organization access"
)
);
}
}

View file

@ -0,0 +1,85 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { resources, apiKeyOrg } from "@server/db";
import { eq, and } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyResourceAccess(
req: Request,
res: Response,
next: NextFunction
) {
const apiKey = req.apiKey;
const resourceId =
req.params.resourceId || req.body.resourceId || req.query.resourceId;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
try {
// Retrieve the resource
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`
)
);
}
if (!resource.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Resource with ID ${resourceId} does not have an organization ID`
)
);
}
// Verify that the API key is linked to the resource's organization
if (!req.apiKeyOrg) {
const apiKeyOrgResult = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, resource.orgId)
)
)
.limit(1);
if (apiKeyOrgResult.length > 0) {
req.apiKeyOrg = apiKeyOrgResult[0];
}
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying resource access"
)
);
}
}

View file

@ -0,0 +1,127 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { roles, apiKeyOrg } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
export async function verifyApiKeyRoleAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const singleRoleId = parseInt(
req.params.roleId || req.body.roleId || req.query.roleId
);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
const { roleIds } = req.body;
const allRoleIds =
roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
if (allRoleIds.length === 0) {
return next();
}
const rolesData = await db
.select()
.from(roles)
.where(inArray(roles.roleId, allRoleIds));
if (rolesData.length !== allRoleIds.length) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"One or more roles not found"
)
);
}
const orgIds = new Set(rolesData.map((role) => role.orgId));
for (const role of rolesData) {
const apiKeyOrgAccess = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, role.orgId!)
)
)
.limit(1);
if (apiKeyOrgAccess.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`Key does not have access to organization for role ID ${role.roleId}`
)
);
}
}
if (orgIds.size > 1) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Roles must belong to the same organization"
)
);
}
const orgId = orgIds.values().next().value;
if (!orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Roles do not have an organization ID"
)
);
}
if (!req.apiKeyOrg) {
// Retrieve the API key's organization link if not already set
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, orgId)
)
)
.limit(1);
if (apiKeyOrgRes.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
req.apiKeyOrg = apiKeyOrgRes[0];
}
return next();
} catch (error) {
logger.error("Error verifying role access:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying role access"
)
);
}
}

View file

@ -0,0 +1,69 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { userOrgs } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeySetResourceUsers(
req: Request,
res: Response,
next: NextFunction
) {
const apiKey = req.apiKey;
const userIds = req.body.userIds;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
if (!userIds) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
}
if (userIds.length === 0) {
return next();
}
try {
const orgId = req.apiKeyOrg.orgId;
const userOrgsData = await db
.select()
.from(userOrgs)
.where(
and(
inArray(userOrgs.userId, userIds),
eq(userOrgs.orgId, orgId)
)
);
if (userOrgsData.length !== userIds.length) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to one or more specified users"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error checking if key has access to the specified users"
)
);
}
}

View file

@ -0,0 +1,89 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import {
sites,
apiKeyOrg
} from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeySiteAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const siteId = parseInt(
req.params.siteId || req.body.siteId || req.query.siteId
);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (isNaN(siteId)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID")
);
}
const site = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (site.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
)
);
}
if (!site[0].orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Site with ID ${siteId} does not have an organization ID`
)
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, site[0].orgId)
)
);
req.apiKeyOrg = apiKeyOrgRes[0];
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site access"
)
);
}
}

View file

@ -0,0 +1,112 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { resources, targets, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyTargetAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const targetId = parseInt(req.params.targetId);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (isNaN(targetId)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID")
);
}
const [target] = await db
.select()
.from(targets)
.where(eq(targets.targetId, targetId))
.limit(1);
if (!target) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Target with ID ${targetId} not found`
)
);
}
const resourceId = target.resourceId;
if (!resourceId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Target with ID ${targetId} does not have a resource ID`
)
);
}
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`
)
);
}
if (!resource.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Resource with ID ${resourceId} does not have an organization ID`
)
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgResult = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, resource.orgId)
)
)
.limit(1);
if (apiKeyOrgResult.length > 0) {
req.apiKeyOrg = apiKeyOrgResult[0];
}
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying target access"
)
);
}
}

View file

@ -0,0 +1,67 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyUserAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const reqUserId =
req.params.userId || req.body.userId || req.query.userId;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!reqUserId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID")
);
}
if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have organization access"
)
);
}
const orgId = req.apiKeyOrg.orgId;
const [userOrgRecord] = await db
.select()
.from(userOrgs)
.where(
and(eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, orgId))
)
.limit(1);
if (!userOrgRecord) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this user"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error checking if key has access to this user"
)
);
}
}

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceAccessToken, resources, userOrgs } from "@server/db/schemas"; import { resourceAccessToken, resources, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { roles, userOrgs } from "@server/db/schemas"; import { roles, userOrgs } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -0,0 +1,96 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const userId = req.user!.userId;
const apiKeyId =
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
const orgId = req.params.orgId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!apiKeyId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);
}
const [apiKey] = await db
.select()
.from(apiKeys)
.innerJoin(apiKeyOrg, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId))
.where(
and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId))
)
.limit(1);
if (!apiKey.apiKeys) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`API key with ID ${apiKeyId} not found`
)
);
}
if (!apiKeyOrg.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`API key with ID ${apiKeyId} does not have an organization ID`
)
);
}
if (!req.userOrg) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, apiKeyOrg.orgId)
)
);
req.userOrg = userOrgRole[0];
req.userRoleIds = userOrgRole.map((r) => r.roleId);
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying key access"
)
);
}
}

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schemas"; import { userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -4,8 +4,8 @@ import {
resources, resources,
userOrgs, userOrgs,
userResources, userResources,
roleResources roleResources,
} from "@server/db/schemas"; } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { roles, userOrgs } from "@server/db/schemas"; import { roles, userOrgs } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,7 +1,7 @@
import { NextFunction, Response } from "express"; import { NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse"; import ErrorResponse from "@server/types/ErrorResponse";
import { db } from "@server/db"; import { db } from "@server/db";
import { users } from "@server/db/schemas"; import { users } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schemas"; import { userOrgs } from "@server/db";
import { and, eq, inArray, or } from "drizzle-orm"; import { and, eq, inArray, or } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -5,8 +5,8 @@ import {
userOrgs, userOrgs,
userSites, userSites,
roleSites, roleSites,
roles roles,
} from "@server/db/schemas"; } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resources, targets, userOrgs } from "@server/db/schemas"; import { resources, targets, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,7 +1,7 @@
import { NextFunction, Response } from "express"; import { NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse"; import ErrorResponse from "@server/types/ErrorResponse";
import { db } from "@server/db"; import { db } from "@server/db";
import { users } from "@server/db/schemas"; import { users } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schemas"; import { userOrgs } from "@server/db";
import { and, eq, or } from "drizzle-orm"; import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schemas"; import { userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -5,9 +5,9 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { resourceAccessToken } from "@server/db/schemas"; import { resourceAccessToken } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import db from "@server/db"; import { db } from "@server/db";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const deleteAccessTokenParamsSchema = z const deleteAccessTokenParamsSchema = z

View file

@ -4,12 +4,12 @@ import {
generateIdFromEntropySize, generateIdFromEntropySize,
SESSION_COOKIE_EXPIRES SESSION_COOKIE_EXPIRES
} from "@server/auth/sessions/app"; } from "@server/auth/sessions/app";
import db from "@server/db"; import { db } from "@server/db";
import { import {
ResourceAccessToken, ResourceAccessToken,
resourceAccessToken, resourceAccessToken,
resources resources
} from "@server/db/schemas"; } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View file

@ -7,7 +7,7 @@ import {
roleResources, roleResources,
resourceAccessToken, resourceAccessToken,
sites sites
} from "@server/db/schemas"; } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View file

@ -0,0 +1,128 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { apiKeyOrg, apiKeys } from "@server/db";
import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import moment from "moment";
import {
generateId,
generateIdFromEntropySize
} from "@server/auth/sessions/app";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({
orgId: z.string().nonempty()
});
const bodySchema = z.object({
name: z.string().min(1).max(255)
});
export type CreateOrgApiKeyBody = z.infer<typeof bodySchema>;
export type CreateOrgApiKeyResponse = {
apiKeyId: string;
name: string;
apiKey: string;
lastChars: string;
createdAt: string;
};
registry.registerPath({
method: "put",
path: "/org/{orgId}/api-key",
description: "Create a new API key scoped to the organization.",
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function createOrgApiKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const { name } = parsedBody.data;
const apiKeyId = generateId(15);
const apiKey = generateIdFromEntropySize(25);
const apiKeyHash = await hashPassword(apiKey);
const lastChars = apiKey.slice(-4);
const createdAt = moment().toISOString();
await db.transaction(async (trx) => {
await trx.insert(apiKeys).values({
name,
apiKeyId,
apiKeyHash,
createdAt,
lastChars
});
await trx.insert(apiKeyOrg).values({
apiKeyId,
orgId
});
});
try {
return response<CreateOrgApiKeyResponse>(res, {
data: {
apiKeyId,
apiKey,
name,
lastChars,
createdAt
},
success: true,
error: false,
message: "API key created",
status: HttpCode.CREATED
});
} catch (e) {
logger.error(e);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create API key"
)
);
}
}

View file

@ -0,0 +1,100 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { apiKeyOrg, apiKeys, orgs } from "@server/db";
import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import moment from "moment";
import {
generateId,
generateIdFromEntropySize
} from "@server/auth/sessions/app";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
const bodySchema = z
.object({
name: z.string().min(1).max(255)
})
.strict();
export type CreateRootApiKeyBody = z.infer<typeof bodySchema>;
export type CreateRootApiKeyResponse = {
apiKeyId: string;
name: string;
apiKey: string;
lastChars: string;
createdAt: string;
};
export async function createRootApiKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { name } = parsedBody.data;
const apiKeyId = generateId(15);
const apiKey = generateIdFromEntropySize(25);
const apiKeyHash = await hashPassword(apiKey);
const lastChars = apiKey.slice(-4);
const createdAt = moment().toISOString();
await db.transaction(async (trx) => {
await trx.insert(apiKeys).values({
apiKeyId,
name,
apiKeyHash,
createdAt,
lastChars,
isRoot: true
});
const allOrgs = await trx.select().from(orgs);
for (const org of allOrgs) {
await trx.insert(apiKeyOrg).values({
apiKeyId,
orgId: org.orgId
});
}
});
try {
return response<CreateRootApiKeyResponse>(res, {
data: {
apiKeyId,
name,
apiKey,
lastChars,
createdAt
},
success: true,
error: false,
message: "API key created",
status: HttpCode.CREATED
});
} catch (e) {
logger.error(e);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create API key"
)
);
}
}

View file

@ -0,0 +1,76 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { apiKeys } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
registry.registerPath({
method: "delete",
path: "/org/{orgId}/api-key/{apiKeyId}",
description: "Delete an API key.",
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
request: {
params: paramsSchema
},
responses: {}
});
export async function deleteApiKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { apiKeyId } = parsedParams.data;
const [apiKey] = await db
.select()
.from(apiKeys)
.where(eq(apiKeys.apiKeyId, apiKeyId))
.limit(1);
if (!apiKey) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`API Key with ID ${apiKeyId} not found`
)
);
}
await db.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId));
return response(res, {
data: null,
success: true,
error: false,
message: "API key deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,99 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { apiKeyOrg, apiKeys } from "@server/db";
import { and, eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const paramsSchema = z.object({
apiKeyId: z.string().nonempty(),
orgId: z.string().nonempty()
});
export async function deleteOrgApiKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { apiKeyId, orgId } = parsedParams.data;
const [apiKey] = await db
.select()
.from(apiKeys)
.where(eq(apiKeys.apiKeyId, apiKeyId))
.innerJoin(
apiKeyOrg,
and(
eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId),
eq(apiKeyOrg.orgId, orgId)
)
)
.limit(1);
if (!apiKey) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`API Key with ID ${apiKeyId} not found`
)
);
}
if (apiKey.apiKeys.isRoot) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot delete root API key"
)
);
}
await db.transaction(async (trx) => {
await trx
.delete(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKeyId),
eq(apiKeyOrg.orgId, orgId)
)
);
const apiKeyOrgs = await db
.select()
.from(apiKeyOrg)
.where(eq(apiKeyOrg.apiKeyId, apiKeyId));
if (apiKeyOrgs.length === 0) {
await trx.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId));
}
});
return response(res, {
data: null,
success: true,
error: false,
message: "API removed from organization",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,76 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { apiKeys } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
async function query(apiKeyId: string) {
return await db
.select({
apiKeyId: apiKeys.apiKeyId,
lastChars: apiKeys.lastChars,
createdAt: apiKeys.createdAt,
isRoot: apiKeys.isRoot,
name: apiKeys.name
})
.from(apiKeys)
.where(eq(apiKeys.apiKeyId, apiKeyId))
.limit(1);
}
export type GetApiKeyResponse = NonNullable<
Awaited<ReturnType<typeof query>>[0]
>;
export async function getApiKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { apiKeyId } = parsedParams.data;
const [apiKey] = await query(apiKeyId);
if (!apiKey) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`API Key with ID ${apiKeyId} not found`
)
);
}
return response<GetApiKeyResponse>(res, {
data: apiKey,
success: true,
error: false,
message: "API key deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,11 @@
export * from "./createRootApiKey";
export * from "./deleteApiKey";
export * from "./getApiKey";
export * from "./listApiKeyActions";
export * from "./listOrgApiKeys";
export * from "./listApiKeyActions";
export * from "./listRootApiKeys";
export * from "./setApiKeyActions";
export * from "./setApiKeyOrgs";
export * from "./createOrgApiKey";
export * from "./deleteOrgApiKey";

View file

@ -0,0 +1,113 @@
import { db } from "@server/db";
import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
const querySchema = z.object({
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.number().int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.number().int().nonnegative())
});
function queryActions(apiKeyId: string) {
return db
.select({
actionId: actions.actionId
})
.from(apiKeyActions)
.where(eq(apiKeyActions.apiKeyId, apiKeyId))
.innerJoin(actions, eq(actions.actionId, apiKeyActions.actionId));
}
export type ListApiKeyActionsResponse = {
actions: Awaited<ReturnType<typeof queryActions>>;
pagination: { total: number; limit: number; offset: number };
};
registry.registerPath({
method: "get",
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
description:
"List all actions set for an API key.",
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
request: {
params: paramsSchema,
query: querySchema
},
responses: {}
});
export async function listApiKeyActions(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const { limit, offset } = parsedQuery.data;
const { apiKeyId } = parsedParams.data;
const baseQuery = queryActions(apiKeyId);
const actionsList = await baseQuery.limit(limit).offset(offset);
return response<ListApiKeyActionsResponse>(res, {
data: {
actions: actionsList,
pagination: {
total: actionsList.length,
limit,
offset
}
},
success: true,
error: false,
message: "API keys retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,116 @@
import { db } from "@server/db";
import { apiKeyOrg, apiKeys } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const querySchema = z.object({
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.number().int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.number().int().nonnegative())
});
const paramsSchema = z.object({
orgId: z.string()
});
function queryApiKeys(orgId: string) {
return db
.select({
apiKeyId: apiKeys.apiKeyId,
orgId: apiKeyOrg.orgId,
lastChars: apiKeys.lastChars,
createdAt: apiKeys.createdAt,
name: apiKeys.name
})
.from(apiKeyOrg)
.where(and(eq(apiKeyOrg.orgId, orgId), eq(apiKeys.isRoot, false)))
.innerJoin(apiKeys, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId));
}
export type ListOrgApiKeysResponse = {
apiKeys: Awaited<ReturnType<typeof queryApiKeys>>;
pagination: { total: number; limit: number; offset: number };
};
registry.registerPath({
method: "get",
path: "/org/{orgId}/api-keys",
description: "List all API keys for an organization",
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
request: {
params: paramsSchema,
query: querySchema
},
responses: {}
});
export async function listOrgApiKeys(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const { limit, offset } = parsedQuery.data;
const { orgId } = parsedParams.data;
const baseQuery = queryApiKeys(orgId);
const apiKeysList = await baseQuery.limit(limit).offset(offset);
return response<ListOrgApiKeysResponse>(res, {
data: {
apiKeys: apiKeysList,
pagination: {
total: apiKeysList.length,
limit,
offset
}
},
success: true,
error: false,
message: "API keys retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,85 @@
import { db } from "@server/db";
import { apiKeys } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm";
const querySchema = z.object({
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.number().int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.number().int().nonnegative())
});
function queryApiKeys() {
return db
.select({
apiKeyId: apiKeys.apiKeyId,
lastChars: apiKeys.lastChars,
createdAt: apiKeys.createdAt,
name: apiKeys.name
})
.from(apiKeys)
.where(eq(apiKeys.isRoot, true));
}
export type ListRootApiKeysResponse = {
apiKeys: Awaited<ReturnType<typeof queryApiKeys>>;
pagination: { total: number; limit: number; offset: number };
};
export async function listRootApiKeys(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const { limit, offset } = parsedQuery.data;
const baseQuery = queryApiKeys();
const apiKeysList = await baseQuery.limit(limit).offset(offset);
return response<ListRootApiKeysResponse>(res, {
data: {
apiKeys: apiKeysList,
pagination: {
total: apiKeysList.length,
limit,
offset
}
},
success: true,
error: false,
message: "API keys retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,136 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { actions, apiKeyActions } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, and, inArray } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const bodySchema = z
.object({
actionIds: z
.array(z.string().nonempty())
.transform((v) => Array.from(new Set(v)))
})
.strict();
const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
registry.registerPath({
method: "post",
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
description:
"Set actions for an API key. This will replace any existing actions.",
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function setApiKeyActions(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { actionIds: newActionIds } = parsedBody.data;
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { apiKeyId } = parsedParams.data;
const actionsExist = await db
.select()
.from(actions)
.where(inArray(actions.actionId, newActionIds));
if (actionsExist.length !== newActionIds.length) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"One or more actions do not exist"
)
);
}
await db.transaction(async (trx) => {
const existingActions = await trx
.select()
.from(apiKeyActions)
.where(eq(apiKeyActions.apiKeyId, apiKeyId));
const existingActionIds = existingActions.map((a) => a.actionId);
const actionIdsToAdd = newActionIds.filter(
(id) => !existingActionIds.includes(id)
);
const actionIdsToRemove = existingActionIds.filter(
(id) => !newActionIds.includes(id)
);
if (actionIdsToRemove.length > 0) {
await trx
.delete(apiKeyActions)
.where(
and(
eq(apiKeyActions.apiKeyId, apiKeyId),
inArray(apiKeyActions.actionId, actionIdsToRemove)
)
);
}
if (actionIdsToAdd.length > 0) {
const insertValues = actionIdsToAdd.map((actionId) => ({
apiKeyId,
actionId
}));
await trx.insert(apiKeyActions).values(insertValues);
}
});
return response(res, {
data: {},
success: true,
error: false,
message: "API key actions updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,117 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { apiKeyOrg, orgs } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, and, inArray } from "drizzle-orm";
const bodySchema = z
.object({
orgIds: z
.array(z.string().nonempty())
.transform((v) => Array.from(new Set(v)))
})
.strict();
const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
export async function setApiKeyOrgs(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { orgIds: newOrgIds } = parsedBody.data;
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { apiKeyId } = parsedParams.data;
// make sure all orgs exist
const allOrgs = await db
.select()
.from(orgs)
.where(inArray(orgs.orgId, newOrgIds));
if (allOrgs.length !== newOrgIds.length) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"One or more orgs do not exist"
)
);
}
await db.transaction(async (trx) => {
const existingOrgs = await trx
.select({ orgId: apiKeyOrg.orgId })
.from(apiKeyOrg)
.where(eq(apiKeyOrg.apiKeyId, apiKeyId));
const existingOrgIds = existingOrgs.map((a) => a.orgId);
const orgIdsToAdd = newOrgIds.filter(
(id) => !existingOrgIds.includes(id)
);
const orgIdsToRemove = existingOrgIds.filter(
(id) => !newOrgIds.includes(id)
);
if (orgIdsToRemove.length > 0) {
await trx
.delete(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKeyId),
inArray(apiKeyOrg.orgId, orgIdsToRemove)
)
);
}
if (orgIdsToAdd.length > 0) {
const insertValues = orgIdsToAdd.map((orgId) => ({
apiKeyId,
orgId
}));
await trx.insert(apiKeyOrg).values(insertValues);
}
return response(res, {
data: {},
success: true,
error: false,
message: "API key orgs updated successfully",
status: HttpCode.OK
});
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

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