Compare commits

...

409 Commits

Author SHA1 Message Date
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
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
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
Milo Schwartz
21f1326045 Merge pull request #651 from fosrl/dev
Dev
2025-05-03 13:07:18 -04:00
miloschwartz
f62e32724c Merge branch 'main' into dev 2025-05-03 12:43:50 -04:00
miloschwartz
5e052a446a 1.3.1 2025-05-03 12:25:02 -04:00
Owen
9a167b5acb Dont overwrite the secret and crowdsec vars 2025-05-02 14:16:10 -04:00
Milo Schwartz
5d2f3186cc Update README.md 2025-05-02 12:25:49 -04:00
miloschwartz
e58d10fc53 fix login form ts error 2025-05-02 11:05:20 -04:00
Milo Schwartz
4392bb604c Merge pull request #636 from fosrl/dev
1.3.0
2025-05-02 10:55:35 -04:00
Milo Schwartz
5a4a6655a5 Merge branch 'main' into dev 2025-05-02 10:55:21 -04:00
miloschwartz
a20befd89f add secret to example config 2025-05-02 10:52:12 -04:00
miloschwartz
a9f0b9aa38 add user checks in routes 2025-05-02 10:44:50 -04:00
Owen
f8e0219b49 Add api 2025-05-01 17:48:49 -04:00
Owen
cb431f3574 Add to readme 2025-05-01 17:45:18 -04:00
miloschwartz
1ff3a9b2f9 improve sidebar rendering 2025-04-30 22:56:10 -04:00
miloschwartz
237960fc5b various small fixes 2025-04-29 22:59:38 -04:00
miloschwartz
3ebc01df8c add migration 2025-04-28 23:07:11 -04:00
miloschwartz
81adcd9234 add async 2025-04-28 22:06:13 -04:00
miloschwartz
cffc156cf6 manually merge wg qr code 2025-04-28 22:05:13 -04:00
Owen
e4af990bf2 Add secret 2025-04-28 21:55:57 -04:00
Owen
e236364124 Change api 2025-04-28 21:50:48 -04:00
miloschwartz
f5a3fd7202 Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-04-28 21:48:13 -04:00
miloschwartz
b3026ba663 update package-lock 2025-04-28 21:48:04 -04:00
miloschwartz
18e6f16ce7 add snippets to create resource 2025-04-28 21:45:43 -04:00
miloschwartz
599d0a52bf add api key code and oidc auto provision code 2025-04-28 21:14:09 -04:00
Owen Schwartz
eed6081ade Merge pull request #558 from TuncTaylan/main
Added dependency of crowdsec to traefik and relaxed its health check
2025-04-28 20:29:17 -04:00
Milo Schwartz
c4ae34383d Merge pull request #595 from imbavirus/feature/wireguard-qrcode
Added QR code to wireguard config for easy scanning on mobile phones
2025-04-28 18:21:01 -04:00
Owen Schwartz
c543376a0a Merge pull request #612 from lxfrdl/feat/enhance_2fa_login
feat: enhance 2fa login usability
2025-04-28 18:18:57 -04:00
Alex Freidel
a5b782b72a feat: enhance 2fa login
As soon as all digits have been entered, the form will be sent automatically. Similar to GitHub's implementation.
2025-04-28 08:14:19 +02:00
miloschwartz
4819f410e6 add license system and ui 2025-04-27 13:03:00 -04:00
Owen Schwartz
4084849fdc Merge pull request #610 from michaelfuckner/main
Fix Typo
2025-04-27 10:24:06 -04:00
Michael Fuckner
35e5f39c71 Fix Typo 2025-04-27 11:33:16 +02:00
miloschwartz
80d76befc9 change terms 2025-04-25 17:13:20 -04:00
Justin van der Westhuizen
893244100e removed react-qr-code 2025-04-25 19:50:14 +02:00
Justin van der Westhuizen
2a43b3ce4a changed qrcode to react-qr-code 2025-04-25 19:46:19 +02:00
Owen Schwartz
b82754c7af Merge pull request #598 from imbavirus/bug/fixed-typo-in-db
Bug: fixed typo in exitNodes table
2025-04-25 10:31:50 -04:00
Owen Schwartz
8793d3976d Merge pull request #599 from TuncTaylan/traefik-update
use the new traefik version 3.3.6
2025-04-25 10:28:42 -04:00
Owen Schwartz
6e833d4cee Merge pull request #582 from vickodin/fix_link_to_geoblock
Improve README: Fix link to geoblock
2025-04-25 10:28:20 -04:00
Taylan
b3d0b69c04 use the new traefik version 3.3.6 2025-04-25 12:01:12 +02:00
Justin van der Westhuizen
28ac5e1237 fixed spelling of public in db (the L was missing) 2025-04-25 08:05:21 +02:00
Justin van der Westhuizen
8990de5618 added missing package 2025-04-25 07:38:17 +02:00
Justin van der Westhuizen
6aeddde1cd Added QR code to wireguard config for easy scanning on mobile phones 2025-04-25 07:06:14 +02:00
miloschwartz
c3dbc64a58 remove hover on badge 2025-04-24 10:42:14 -04:00
vickodin
2a00c877ea Improve README: Fix link to geoblock 2025-04-24 09:07:16 +03:00
miloschwartz
91b4bb4683 move proxy related settings to new proxy tab for resource 2025-04-23 23:08:25 -04:00
miloschwartz
f4fd33b47f Merge branch 'auth-providers' into dev 2025-04-23 22:08:37 -04:00
miloschwartz
d6d6a59eee add new resource create wizard 2025-04-23 20:58:53 -04:00
miloschwartz
4dba75f913 obscure pin code input closes #580 2025-04-23 16:33:55 -04:00
miloschwartz
548a883e3f show smtp note on whitelist 2025-04-23 16:24:49 -04:00
miloschwartz
a6d6aaaadd add api keys to sidebar nav 2025-04-23 16:18:51 -04:00
miloschwartz
566e66daa4 fix org landing 2025-04-23 15:52:02 -04:00
miloschwartz
97af632c61 add option to pre provision idp user 2025-04-23 15:44:27 -04:00
Taylan
5d6e15b0d6 indentation fix 2025-04-23 20:54:03 +02:00
Taylan
419bacf55f check and add the service dependency of crowdsec to traefik 2025-04-23 20:36:16 +02:00
miloschwartz
960eb34c7d refactor invite user to create wizard 2025-04-23 13:46:06 -04:00
miloschwartz
6f59d0cd2d add createOrgUser endpoint 2025-04-23 13:26:38 -04:00
Taylan
6fd1dbc638 some more warning about indirect requirements 2025-04-23 18:39:48 +02:00
Taylan
87915f29f6 correct formated the file 2025-04-23 18:37:36 +02:00
Taylan
181071e4f6 refactoring multiple used code parts, cleared some warnings and added more error checking 2025-04-23 18:35:15 +02:00
miloschwartz
feb558cfa8 testing 2025-04-22 22:53:52 -04:00
Milo Schwartz
9ea7c43212 Merge pull request #511 from x86txt/sticky_targets
Add ability for sticky sessions to backend resource.
2025-04-21 22:28:45 -04:00
Milo Schwartz
38528ae8c5 Merge branch 'dev' into sticky_targets 2025-04-21 22:28:18 -04:00
Taylan
c837899d82 Relaxed health check for crowdsec and dependance to traefik 2025-04-21 11:29:56 +02:00
miloschwartz
7938b419cc add actions for idp 2025-04-20 21:31:01 -04:00
miloschwartz
bf8bb1a0df adjustment to pr 2025-04-20 20:50:50 -04:00
Milo Schwartz
957fa67e24 Merge pull request #501 from achtnullzwei/customize-tls-server-name
Add option to customise TLS server name in resource settings
2025-04-20 17:54:18 -04:00
miloschwartz
b4c6897850 Merge branch 'dev' into auth-providers 2025-04-20 15:50:37 -04:00
miloschwartz
e2f056e6ca add idp to profile dropdown 2025-04-20 15:50:17 -04:00
miloschwartz
8fa719181a show list of idp on login 2025-04-18 21:59:58 -04:00
miloschwartz
b4fda6a1f6 add default mapping policy and move auto provision 2025-04-18 17:04:16 -04:00
miloschwartz
99188233db create, delete, and update idp org policies 2025-04-18 15:38:50 -04:00
miloschwartz
3bab90891f move improvements to layout 2025-04-18 11:36:34 -04:00
miloschwartz
8c0e4d2d8c edit oidc idp general tab 2025-04-17 22:36:16 -04:00
miloschwartz
3e94384cde add info box to admin users table 2025-04-17 21:21:41 -04:00
miloschwartz
189b739997 add create idp wizard for oidc 2025-04-16 22:44:57 -04:00
miloschwartz
334fc55dd0 add server admin button 2025-04-16 21:37:15 -04:00
miloschwartz
ab933d48de more prominent sidebar sections 2025-04-16 20:49:06 -04:00
miloschwartz
36b62a5fe4 fix animations 2025-04-16 20:38:00 -04:00
Milo Schwartz
08752820fc Merge pull request #535 from TuncTaylan/main
Turkish translation
2025-04-16 20:18:27 -04:00
Taylan
787ec50a9c turkish translation of Main “General” 2025-04-16 21:48:50 +02:00
Taylan
65b29161a0 turkish translation of Create Shareable Link popup 2025-04-16 21:47:53 +02:00
Taylan
f60f15345f turkish translation of shareable links -> Content 2025-04-16 21:47:17 +02:00
Taylan
c286c28d46 turkish translation of Main “Shareable Links” -> hero 2025-04-16 21:46:38 +02:00
Taylan
8fb003d7ce turkish translation of Invite User popup 2025-04-16 21:45:47 +02:00
Taylan
35daf42a55 turkish translation of Main “User & Roles” -> Content 2025-04-16 21:45:14 +02:00
Taylan
976aaca287 turkish translation of Add Resource Popup 2025-04-16 21:43:56 +02:00
Taylan
0454f09383 turkish translation of main->content 2025-04-16 21:43:12 +02:00
Taylan
6b5674a107 turkish translation of main ressources / hero 2025-04-16 21:42:30 +02:00
Taylan
45a75d0bee turkish translation of Add Site Popup 2025-04-16 21:39:42 +02:00
Taylan
12f627711c turkish translation of content 2025-04-16 21:38:44 +02:00
Taylan
442775ac90 turkish translation of hero section 2025-04-16 21:37:56 +02:00
Taylan
01da3b3225 turkish translation of footer 2025-04-16 21:36:18 +02:00
Taylan
51ac815b23 turkish translation of navbar 2025-04-16 21:34:49 +02:00
Taylan
285ad45a0e turkish translation of organization selector 2025-04-16 21:33:41 +02:00
Taylan
4707722e6e turkish translation of shared header 2025-04-16 21:33:06 +02:00
Taylan
499f75edd1 turkish translation of section organization site after login 2025-04-16 21:32:26 +02:00
Taylan
57b96adcd0 turkish translation of section login site 2025-04-16 21:31:36 +02:00
Taylan
eb9675c6cf Turkish translation of section authentication site 2025-04-16 21:30:45 +02:00
Taylan
b59c6e377a German translation of section Authentication size 2025-04-16 21:27:48 +02:00
miloschwartz
432f38333e add create, delete, list for idp org policy 2025-04-15 10:16:15 -04:00
miloschwartz
e86640547e add delete, get, list idp 2025-04-15 09:26:25 -04:00
Matthias Palmetshofer
25c125b96d added advanced section to general page & custom host header field 2025-04-15 13:17:46 +02:00
miloschwartz
aa3b527f67 add validate callback loading state and encryption 2025-04-14 20:56:45 -04:00
miloschwartz
bacd5a4373 add child to user nav bar item 2025-04-13 18:30:14 -04:00
miloschwartz
53be2739bb successful log in loop poc 2025-04-13 18:29:23 -04:00
Matthew Evans
4a42aa385a Update sticky session to show only with 2+ targets and use IP strategy for TCP 2025-04-13 15:29:58 -04:00
Matthew Evans
ac8e315fbd Add ability for sticky sessions to backend resource. 2025-04-13 15:29:58 -04:00
miloschwartz
7556a59e11 Merge branch 'dev' into auth-providers 2025-04-13 14:49:02 -04:00
miloschwartz
8b0c30f19f more tweaks to layout 2025-04-13 14:46:13 -04:00
miloschwartz
b731a50cc9 nested sidebar 2025-04-13 14:30:20 -04:00
miloschwartz
2398931cc1 fix ref type error 2025-04-13 14:30:19 -04:00
miloschwartz
419e576a3e adjust border 2025-04-13 14:30:19 -04:00
miloschwartz
1a750e8279 test new layout 2025-04-13 14:30:19 -04:00
miloschwartz
f14379a1c8 remove forward ref 2025-04-13 14:30:19 -04:00
miloschwartz
521bbbf1d6 update to tw4 2025-04-13 14:30:19 -04:00
Owen
cb775340a4 Merge branch 'main' into dev 2025-04-13 10:57:48 -04:00
Owen Schwartz
31bd42f964 Merge pull request #519 from TuncTaylan/main
More relaxed healthcheck for low end systems
2025-04-13 10:56:28 -04:00
Taylan
e64e7d1d92 More relaxed healthcheck for low end systems 2025-04-13 16:51:00 +02:00
miloschwartz
480a5f648d testing oidc callback 2025-04-12 15:44:26 -04:00
miloschwartz
9cb215295a make supporter star a little more minimal 2025-04-12 12:34:16 -04:00
miloschwartz
764c56c4a1 makes sidebar width full 2025-04-12 12:24:16 -04:00
miloschwartz
e057c5f3bf use mobile friendly dialog in regenerate invite 2025-04-12 12:18:05 -04:00
Milo Schwartz
bc8cd5c941 Merge pull request #496 from grokdesigns/add-invitation-management
Add invitation management
2025-04-12 12:12:03 -04:00
Taylan
6350edf8fd updated the traefik image to v3.3.5
updated the crowdsec-bouncer-traefik-plugin to v1.4.2
added default crowdsecAppsecBodyLimit value for bigger files
2025-04-12 10:07:06 -04:00
Owen Schwartz
8e8fdabd03 Merge pull request #515 from TuncTaylan/main
updated the traefik image and crowdsec-bouncer-traefik-plugin for bigger files
2025-04-12 10:06:09 -04:00
miloschwartz
2883d8c544 fix typo 2025-04-11 20:21:55 -04:00
Taylan
dd8c426faa updated the traefik image to v3.3.5
updated the crowdsec-bouncer-traefik-plugin to v1.4.2
added default crowdsecAppsecBodyLimit value for bigger files
2025-04-11 22:07:20 +02:00
Matthias Palmetshofer
64a2cc23c6 adjusting field description; fix migration script; trying to resolve conflict in updateResource.ts 2025-04-11 09:52:34 +02:00
Owen
ec33fe5657 Merge branch 'main' into dev 2025-04-10 22:17:27 -04:00
Owen
56b3b2ab3b Reduce to 14 days 2025-04-10 22:16:40 -04:00
Owen Schwartz
a436dff4a0 Merge pull request #504 from Jester0027/feature/newt-podman-install
feat(sites): Provide Podman run and Podman Quadlet setup for Newt
2025-04-10 21:40:35 -04:00
Owen Schwartz
cf80d67bf8 Merge branch 'dev' into feature/newt-podman-install 2025-04-10 21:40:23 -04:00
paul
e24edc0803 feat(sites): Provide Podman run and Podman Quadlet setup for Newt 2025-04-10 23:33:28 +02:00
grokdesigns
d89ca10a82 Had to update child menu item logic
Noticed that submenus weren't rendered on mobile. Updated logic to make children work with mobile dropdown
2025-04-10 07:43:51 -07:00
grokdesigns
d9e6d0c71a Add regenerate to invitation functionality, see pull request details 2025-04-09 20:32:21 -07:00
Matthias Palmetshofer
517bc7f632 added table change to new migration script 2025-04-10 00:36:34 +02:00
Matthias Palmetshofer
674316aa46 add option to set TLS Server Name 2025-04-09 23:42:50 +02:00
grokdesigns
7a55c9ad03 Add support for menu children and moved invitations under users 2025-04-09 09:23:47 -07:00
grokdesigns
c7f3c9da92 Removed leftover section from earlier testing 2025-04-08 18:34:21 -07:00
Owen Schwartz
be77b3e8f3 Merge pull request #493 from grokdesigns/fix-popover-issue
Fix black box issue on popover
2025-04-08 21:26:56 -04:00
grokdesigns
d7f50bac6a Add invitation management 2025-04-08 18:18:57 -07:00
Owen
3ccfe60685 Merge branch 'grokdesigns-update-package-resolve-vulns' into dev 2025-04-08 21:04:22 -04:00
grokdesigns
40040af957 Bump react-email version 2025-04-08 13:36:44 -07:00
grokdesigns
1568b38eac Fix black box issue on popover 2025-04-08 12:53:12 -07:00
grokdesigns
7fd1652a71 Update libraries to resolve critical security findings 2025-04-08 12:51:06 -07:00
Owen Schwartz
787a172a7c Merge pull request #477 from grokdesigns/feature-add-sponsor-message
Add supporter message feature
2025-04-08 11:23:41 -04:00
grokdesigns
23a68fbc10 Add confetti on valid key and make thank you less intrusive 2025-04-07 21:15:24 -07:00
Owen
f078ee6051 Merge branch 'main' into dev 2025-04-07 21:16:05 -04:00
Owen
0450f62108 Merge branch 'Lokowitz-fix-crowdsec-config' 2025-04-07 21:15:47 -04:00
grokdesigns
b2faeb3c17 Added some more pizazz to the thank you message 2025-04-07 14:38:49 -07:00
grokdesigns
9ea37789d6 Applied Prettier formatting 2025-04-07 13:32:07 -07:00
grokdesigns
aa45150c51 Add supporter message feature 2025-04-07 10:34:32 -07:00
Owen
a708750fea Increase retries to 15
Resolves #327
2025-04-07 09:56:29 -04:00
miloschwartz
d260450a84 add openapi registers 2025-04-06 22:44:14 -04:00
miloschwartz
a76e3e00f7 openapi test 2025-04-06 16:06:50 -04:00
Marvin
a33ebe5bc5 Merge branch 'fosrl:main' into fix-crowdsec-config 2025-04-06 19:31:54 +02:00
Milo Schwartz
6f683ca486 Merge pull request #467 from fosrl/dev
1.2.0
2025-04-06 13:11:02 -04:00
miloschwartz
0e65f8c921 check resource id on verify access token 2025-04-06 13:08:55 -04:00
Owen
dfcab90c2d Add permissions 2025-04-06 12:05:13 -04:00
Owen
5a6a035d30 Add permissions 2025-04-06 12:04:42 -04:00
Owen
d76ff17fb3 Add stale bot 2025-04-06 12:02:22 -04:00
Owen
1f570e9b46 Add stale bot 2025-04-06 12:01:37 -04:00
miloschwartz
4953e69b1b comment out old create newt endpoint 2025-04-06 11:48:42 -04:00
miloschwartz
ab6ecdbc9c update config file templates 2025-04-06 11:46:08 -04:00
miloschwartz
0b7ca95d21 update badger in migration 2025-04-06 11:39:16 -04:00
miloschwartz
6cc4bc2645 add pass access token in headers 2025-04-05 22:36:51 -04:00
Marvin
b75f848b90 Update crowdsec.go 2025-04-05 17:47:37 +02:00
Marvin
c4e62a7aee Update docker-compose.yml 2025-04-05 17:45:43 +02:00
Marvin
c903c03979 Delete install/config/crowdsec/acquis.yaml 2025-04-05 17:44:38 +02:00
Marvin
d7b9755f3a Create appsec.yaml 2025-04-05 17:44:20 +02:00
Marvin
e17bf0db13 Create traefik.yaml 2025-04-05 17:43:54 +02:00
miloschwartz
74d6b3d902 shorten share links and add migration 2025-04-04 22:58:01 -04:00
Owen
302094771b Merge branch 'main' into dev 2025-04-01 22:49:39 -04:00
miloschwartz
80ef8f189e replace old error formatting with zod format error 2025-03-31 22:37:39 -04:00
miloschwartz
6204fa0ade fix update server admin email cause create new user closes #443 2025-03-31 15:03:21 -04:00
miloschwartz
1d105fc5be fix count in list domains endpoint 2025-03-31 12:51:08 -04:00
miloschwartz
3612857585 use global search for tables 2025-03-31 10:31:35 -04:00
miloschwartz
8f1ee60119 fix showing not protected when only password enabled closes #129 2025-03-31 10:16:34 -04:00
miloschwartz
e7ca7fe89c add toggle resource visibility closes #442 2025-03-31 10:10:54 -04:00
Owen Schwartz
4be1d87602 Merge pull request #419 from hareland/fix-bandwidth
fix(bandwidth): Record correct megaBytesIn/megaBytesOut
2025-03-30 11:04:00 -04:00
hareland
131df8aeb7 fix(bandwidth): use correct flipping and addition 2025-03-28 14:54:37 +01:00
hareland
3442942893 fix(bandwidth): Record correct megaBytesIn/megaBytesOut 2025-03-28 12:06:17 +01:00
miloschwartz
fbd78ab842 add new checkbox variants 2025-03-27 23:12:36 -04:00
miloschwartz
66f324e18c fix typo in form description 2025-03-27 17:42:51 -04:00
miloschwartz
5e2f9e1eeb add createNewt action and remove max orgs restriction 2025-03-26 22:20:22 -04:00
miloschwartz
fefb07e14c move schema.ts to module 2025-03-23 17:11:48 -04:00
miloschwartz
013f342ff6 fix broken header layout on mobile 2025-03-23 12:37:57 -04:00
miloschwartz
aabdcea3c0 add docs link 2025-03-22 12:37:35 -04:00
miloschwartz
a178faa377 add support links 2025-03-22 12:35:33 -04:00
miloschwartz
edf0ce226f Merge branch 'main' into dev 2025-03-22 12:25:00 -04:00
miloschwartz
7118ae374d fix try catch in supporter keys 2025-03-22 12:24:20 -04:00
miloschwartz
f2a14e6a36 append timestamp to cookie name to prevent redirect loops 2025-03-21 21:38:36 -04:00
miloschwartz
f37be774a6 disable limited tier if already used 2025-03-21 18:36:11 -04:00
miloschwartz
0dcfeb3587 add server admin panel to delete users 2025-03-21 18:04:27 -04:00
Owen
dbfc8b51aa Update default email 2025-03-21 17:18:51 -04:00
miloschwartz
d72a8af04b log failed check key 2025-03-21 17:14:27 -04:00
Owen
7131dea7a0 package-lock update 2025-03-21 17:12:59 -04:00
Owen
deb30ed4ae Add admin user api interfaces 2025-03-21 17:05:04 -04:00
Owen
3b09ef3345 False by default for crowdsec 2nd question 2025-03-21 09:57:28 -04:00
miloschwartz
06e90c9555 don't add wildcard asterisk for base domain resource closes #356 2025-03-20 22:52:29 -04:00
miloschwartz
cdc415079c add supporer key program 2025-03-20 22:16:02 -04:00
miloschwartz
1c2ba4076a add crowdsec warning to installer 2025-03-19 21:17:19 -04:00
miloschwartz
af68aa692c fix header padding on large screens 2025-03-17 11:53:30 -04:00
miloschwartz
edba818615 add new create site workflow 2025-03-16 15:20:19 -04:00
miloschwartz
cdf904a2bc fix broken docs link closes #335 2025-03-15 17:37:27 -04:00
miloschwartz
fedab6c9a8 Merge branch 'main' into dev 2025-03-11 21:03:25 -04:00
Milo Schwartz
33e8ed4c93 Update README.md 2025-03-11 21:02:42 -04:00
miloschwartz
2b54dfe035 fix rules info columns 2025-03-10 12:47:42 -04:00
Milo Schwartz
e601816791 Merge pull request #319 from fosrl/dev
1.0.1
2025-03-10 11:09:38 -04:00
miloschwartz
7a46cf3da7 add border-2 to checkbox 2025-03-10 11:06:02 -04:00
miloschwartz
ad32e5e651 fix base domain overwritten on update closes #282 2025-03-09 22:05:13 -04:00
miloschwartz
8ec55eb70d append site name to resource in list add add site to info card closes #297 2025-03-08 22:11:27 -05:00
Owen
767fec19cd Remove old example config 2025-03-08 20:03:26 -05:00
miloschwartz
d215a12f5a disable auto backup on migration with env var 2025-03-08 18:23:36 -05:00
Owen
d22dcfb464 Optimize container size 2025-03-08 18:11:47 -05:00
miloschwartz
c93b36c757 remove environment variable support and config file autogeneration 2025-03-08 18:06:14 -05:00
Owen Schwartz
9253dd19ba Merge pull request #307 from fosrl/remove_json_group_array
Remove json_group_array from queries
2025-03-08 17:30:46 -05:00
miloschwartz
b9d83a2507 add es.md 2025-03-08 16:06:52 -05:00
Owen
581f96daa8 Remove json_group_array from queries
Also improve handling tcp/udp resources in newt function so it does not
loop twice
2025-03-08 11:58:48 -05:00
Owen
33ff2fbf3b Allow matching parts of words in path
Resolves #228
2025-03-08 11:43:47 -05:00
Owen
535b4e1fb1 Add clean up our few tests and add tests for #228 2025-03-08 11:43:22 -05:00
Owen
5871bea706 Reset port when entering targets
Resolves #270
2025-03-08 10:52:38 -05:00
Owen
07eb422491 Add back metrics port for scripting 2025-03-04 23:52:37 -05:00
Owen
654ed46a46 Return 401 instead of 400 on bad login
Resolves #276
2025-03-04 20:32:48 -05:00
Milo Schwartz
eb73da8aa0 Merge pull request #275 from fosrl/dev
add migration script
2025-03-04 11:14:31 -05:00
miloschwartz
cc6800c791 add migration script 2025-03-04 11:13:34 -05:00
Milo Schwartz
47abdf873a Merge pull request #274 from fosrl/dev
1.0.0
2025-03-04 11:12:14 -05:00
miloschwartz
90366da61b allow hash in url path rule 2025-03-03 19:47:07 -05:00
miloschwartz
5529beaf6e allow anything for hostname closes #265 2025-03-03 17:11:41 -05:00
miloschwartz
93c8236535 update example config files 2025-03-03 17:09:48 -05:00
Owen
37fdc4a6a8 Warn if it did not replace the bouncer key 2025-03-03 15:43:26 -05:00
miloschwartz
a456a37b2f fix typo 2025-03-02 23:24:21 -05:00
miloschwartz
430afe3f93 Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-03-02 22:19:11 -05:00
miloschwartz
3b60e1f3ac update screenshots 2025-03-02 22:19:04 -05:00
Owen
b8543e5fa8 Remove prom exposed port and waf port
Resolves #247
2025-03-02 21:49:42 -05:00
Owen
d594314e52 Remove gerbil depenency and exposed port 9090 2025-03-02 21:47:32 -05:00
Owen
81c142e8ae Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-03-02 21:46:22 -05:00
miloschwartz
59eedce664 allow setting tks.rejectUnauthorized for Nodemailer in config closes #264 2025-03-02 20:03:20 -05:00
miloschwartz
adef93623d more visual enhancements and use expires instead of max age in cookies 2025-03-02 15:50:03 -05:00
miloschwartz
759434e9f8 more visual enhancements and update readme 2025-03-01 23:03:42 -05:00
miloschwartz
0e38f58a7f minor visual enhancements 2025-03-01 17:45:38 -05:00
miloschwartz
8445e83c7c Merge branch 'dev' 2025-02-27 11:02:36 -05:00
miloschwartz
89a59b25fc Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-02-27 11:02:10 -05:00
miloschwartz
57a37a01ce full width settings for user 2025-02-27 11:01:53 -05:00
Owen
f8add1f098 Fix cicd with go-build-release installer 2025-02-27 10:59:16 -05:00
Owen
0bd0cc76fb Fix cicd with go-build-release installer 2025-02-27 10:57:37 -05:00
Milo Schwartz
06e4fbac68 Merge pull request #244 from fosrl/dev
multiple domains, crowdsec, and more
2025-02-27 10:52:06 -05:00
miloschwartz
e82df67063 Merge branch 'dev' into multi-domain 2025-02-26 21:26:20 -05:00
miloschwartz
84f94bb727 Merge branch 'multi-domain' of https://github.com/fosrl/pangolin into multi-domain 2025-02-26 21:25:00 -05:00
miloschwartz
20f1a6372b small visual improvements 2025-02-26 21:24:35 -05:00
Owen
06c434a5ea Copy in the right versions when building 2025-02-26 21:13:52 -05:00
Owen Schwartz
b83dadb14b Merge pull request #243 from fosrl/crowdsec
Crowdsec
2025-02-26 20:55:51 -05:00
Owen
492e53edf3 Merge branch 'dev' into crowdsec 2025-02-26 20:52:52 -05:00
Owen
3d9557b65c Merge branch 'main' into dev 2025-02-26 20:49:33 -05:00
Owen
332804ed71 Add base_domain new config 2025-02-25 23:41:43 -05:00
miloschwartz
de70c62ea8 adjustments to migration after testing 2025-02-25 22:58:52 -05:00
miloschwartz
e4789c6b08 always check rules even if auth is disabled 2025-02-24 22:52:38 -05:00
miloschwartz
ec9d02a735 clear stale data from db on restart 2025-02-24 22:46:55 -05:00
miloschwartz
ae73a2f3f4 show more than 10 rows in rules and targets table closes #221 2025-02-24 22:32:22 -05:00
miloschwartz
d8183bfd0d add sql migration 2025-02-24 22:25:42 -05:00
miloschwartz
e11748fe30 minor bug files, remove unqiue constraint, and start migration 2025-02-24 22:06:21 -05:00
Milo Schwartz
ccbe56e110 update README.md 2025-02-24 12:03:48 -05:00
miloschwartz
ff37e07ce6 make cookies work with multi-domain 2025-02-23 23:04:01 -05:00
Owen
f59f0ee57d Merge yaml files instead? 2025-02-23 21:44:02 -05:00
Milo Schwartz
372932985d update README.md 2025-02-23 17:34:28 -05:00
miloschwartz
c877bb1187 bug fixes to smooth out multi domain inputs forms 2025-02-19 23:00:59 -05:00
Owen
5f95500b6f Crowdsec installer works? 2025-02-19 21:42:42 -05:00
Owen
3194dc56eb Move path to first in dropdown 2025-02-19 09:44:40 -05:00
miloschwartz
e49fb646b0 refactor subdomain inputs 2025-02-18 22:56:46 -05:00
Owen
fd11fb81d6 Remove some config 2025-02-18 21:41:23 -05:00
miloschwartz
82f990eb8b add list domains for org endpoint 2025-02-16 18:09:17 -05:00
miloschwartz
851bedb2e5 refactor create and update resource endpoints 2025-02-16 17:28:10 -05:00
Owen
e6c42e9610 Indent 2 2025-02-16 11:31:01 -05:00
Owen
d3d523b2b8 Refactor docker copy and keep entrypoints 2025-02-16 11:26:45 -05:00
miloschwartz
532d3696c2 sync config managed domains to db 2025-02-15 22:41:39 -05:00
Owen
dabd4a055c Creating structure correctly 2025-02-15 22:00:31 -05:00
Owen
7bf820a4bf Clean off ports for 80 and 443 hosts 2025-02-15 17:48:27 -05:00
Owen
b862e1aeef Add h2c as target method; Resolves #115 2025-02-15 17:43:44 -05:00
Owen
bdee036ab4 Add name; Resolves #190 2025-02-15 17:08:11 -05:00
Milo Schwartz
62238948e0 save 2025-02-14 20:22:26 -05:00
Milo Schwartz
489f6bed17 Merge pull request #202 from fosrl/dev
hotfixes coming from beta13
2025-02-14 16:53:58 -05:00
Milo Schwartz
6aa4908446 bump version 2025-02-14 16:53:05 -05:00
Milo Schwartz
d5a220a004 create target validator and add url validator 2025-02-14 16:46:46 -05:00
Owen
a418195b28 Fix ip range pick initial range; add test 2025-02-14 15:49:40 -05:00
Milo Schwartz
2ff6d1d117 allow any string as target 2025-02-14 13:27:34 -05:00
Milo Schwartz
8dd30c88ab fix reset password sql error 2025-02-14 13:12:29 -05:00
Owen
7797c6c770 Allow the chars from RFC 3986 2025-02-14 12:38:28 -05:00
Owen
40922fedb8 Support v6 2025-02-14 12:32:18 -05:00
Milo Schwartz
4c1366ef91 force router refresh on save closes #198 2025-02-14 12:27:03 -05:00
Owen
f61d442989 Allow . in path; resolves #199 2025-02-14 09:51:17 -05:00
Owen
60449afca5 Reorg; create crowdsec folder properly now 2025-02-13 22:16:52 -05:00
Owen
81c4199e87 Complete bash migration 2025-02-12 21:56:13 -05:00
Owen
a7b8ffaf9f Merge branch 'dev' into crowdsec 2025-02-10 21:34:25 -05:00
Owen
d22c7826fe Add config files 2025-02-09 16:14:29 -05:00
478 changed files with 48570 additions and 6591 deletions

View File

@@ -22,7 +22,6 @@ next-env.d.ts
*.log
.machinelogs*.json
*-audit.json
package-lock.json
install/
bruno/
LICENSE

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"

View File

@@ -64,7 +64,7 @@ jobs:
- name: Build installer
working-directory: install
run: |
make release
make go-build-release
- name: Upload artifacts from /install/bin
uses: actions/upload-artifact@v4

37
.github/workflows/stale-bot.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Mark and Close Stale Issues
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch: # Allow manual trigger
permissions:
contents: write # only for delete-branch option
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 14
days-before-close: 14
stale-issue-message: 'This issue has been automatically marked as stale due to 14 days of inactivity. It will be closed in 14 days if no further activity occurs.'
close-issue-message: 'This issue has been automatically closed due to inactivity. If you believe this is still relevant, please open a new issue with up-to-date information.'
stale-issue-label: 'stale'
exempt-issue-labels: 'needs investigating, networking, new feature, reverse proxy, bug, api, authentication, documentation, enhancement, help wanted, good first issue, question'
exempt-all-issue-assignees: true
only-labels: ''
exempt-pr-labels: ''
days-before-pr-stale: -1
days-before-pr-close: -1
operations-per-run: 100
remove-stale-when-updated: true
delete-branch: false
enable-statistics: true

2
.gitignore vendored
View File

@@ -23,7 +23,6 @@ next-env.d.ts
.machinelogs*.json
*-audit.json
migrations
package-lock.json
tsconfig.tsbuildinfo
config/config.yml
dist
@@ -33,3 +32,4 @@ installer
bin
.secrets
test_event.json
.idea/

View File

@@ -2,35 +2,36 @@ FROM node:20-alpine AS builder
WORKDIR /app
# COPY package.json package-lock.json ./
COPY package.json ./
RUN npm install
COPY . .
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schema.ts --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
RUN apk add --no-cache curl
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 install --only=production && npm cache clean --force
RUN npm install --omit=dev
COPY --from=builder /app/.next ./.next
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 config/config.example.yml ./dist/config.example.yml
COPY config/traefik/traefik_config.example.yml ./dist/traefik_config.example.yml
COPY config/traefik/dynamic_config.example.yml ./dist/dynamic_config.example.yml
COPY server/db/names.json ./dist/names.json
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 install
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 install --only=production && 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

@@ -5,6 +5,8 @@ build-release:
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:$(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:
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .

166
README.md
View File

@@ -1,7 +1,34 @@
<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>
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
</picture>
</h2>
</div>
<h4 align="center">Tunneled Reverse Proxy Server with Access Control</h4>
<div align="center">
_Your own self-hosted zero trust tunnel._
</div>
<div align="center">
<h5>
<a href="https://fossorial.io">
Website
</a>
<span> | </span>
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
Install Guide
</a>
<span> | </span>
<a href="mailto:numbat@fossorial.io">
Contact Us
</a>
</h5>
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.fossorial.io/)
[![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)
@@ -9,134 +36,103 @@
</div>
<div align="center">
<h5>
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
Install Guide
</a>
<span> | </span>
<a href="https://docs.fossorial.io">
Full Documentation
</a>
</h5>
</div>
<h3 align="center">Tunneled Mesh Reverse Proxy Server with Access Control</h3>
<div align="center">
_Your own self-hosted zero trust tunnel._
</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.
<img src="public/screenshots/sites.png" alt="Preview"/>
<img src="public/screenshots/hero.png" alt="Preview"/>
_Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected to the central server._
_Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._
## Key Features
### Reverse Proxy Through WireGuard Tunnel
- Expose private resources on your network **without opening ports** (firewall punching).
- Secure and easy to configure site-to-site connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
- Built-in support for any WireGuard client.
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
- Load balancing.
- Expose private resources on your network **without opening ports** (firewall punching).
- Secure and easy to configure site-to-site connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
- Built-in support for any WireGuard client.
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
- Load balancing.
### Identity & Access Management
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
- TOTP with backup codes for two-factor authentication.
- Create organizations, each with multiple sites, users, and roles.
- **Role-based access control** to manage resource access permissions.
- Additional authentication options include:
- Email whitelisting with **one-time passcodes.**
- **Temporary, self-destructing share links.**
- Resource specific pin codes.
- Resource specific passwords.
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
- TOTP with backup codes for two-factor authentication.
- Create organizations, each with multiple sites, users, and roles.
- **Role-based access control** to manage resource access permissions.
- Additional authentication options include:
- Email whitelisting with **one-time passcodes.**
- **Temporary, self-destructing share links.**
- Resource specific pin codes.
- Resource specific passwords.
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
- Auto-provision users and roles from your IdP.
### Simple Dashboard UI
- Manage sites, users, and roles with a clean and intuitive UI.
- Monitor site usage and connectivity.
- Light and dark mode options.
- Mobile friendly.
- Manage sites, users, and roles with a clean and intuitive UI.
- Monitor site usage and connectivity.
- Light and dark mode options.
- Mobile friendly.
### Easy Deployment
- Run on any cloud provider or on-premises.
- Docker Compose based setup for simplified deployment.
- Future-proof installation script for streamlined setup and feature additions.
- Use your preferred WireGuard client to connect, or use Newt, our custom user space client for the best experience.
- Run on any cloud provider or on-premises.
- **Docker Compose based setup** for simplified deployment.
- Future-proof installation script for streamlined setup and feature additions.
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
- Use the API to create custom integrations and scripts.
- Fine-grained access control to the API via scoped API keys.
- Comprehensive Swagger documentation for the API.
### Modular Design
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [Fail2Ban](https://plugins.traefik.io/plugins/628c9ebcffc0cd18356a979f/fail2-ban) or [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin).
- Attach as many sites to the central server as you wish.
- 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.**
- Attach as many sites to the central server as you wish.
## Screenshots
<div align="center">
<table>
<tr>
<td align="center"><img src="public/screenshots/sites.png" alt="Sites Example" width="200"/></td>
<td align="center"><img src="public/screenshots/users.png" alt="Users Example" width="200"/></td>
<td align="center"><img src="public/screenshots/share-link.png" alt="Share Link Example" width="200"/></td>
</tr>
<tr>
<td align="center"><b>Sites</b></td>
<td align="center"><b>Users</b></td>
<td align="center"><b>Share Link</b></td>
</tr>
<tr>
<td align="center"><img src="public/screenshots/auth.png" alt="Authentication Example" width="200"/></td>
<td align="center"><img src="public/screenshots/connectivity.png" alt="Connectivity Example" width="200"/></td>
<td align="center"></td>
</tr>
<tr>
<td align="center"><b>Authentication</b></td>
<td align="center"><b>Connectivity</b></td>
<td align="center"><b></b></td>
</tr>
</table>
</div>
<img src="public/screenshots/collage.png" alt="Collage"/>
## Deployment and Usage Example
1. **Deploy the Central Server**:
- Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like 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.
2. **Domain Configuration**:
> [!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 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.
1. **Domain Configuration**:
- 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.
- Automatically establish a connection from these sites to the central server.
4. **Configure Users & Roles**
3. **Expose Resources**:
- Define organizations and invite users.
- Implement user- or role-based permissions to control resource access.
- Add resources to the central server and configure access control rules.
- Access these resources securely from anywhere.
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
**Use Case Example - Deploying Services For Your Business**:
You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IdP solution. Expose resources on prem and on the cloud.
**Use Case Example - IoT Networks**:
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
## Similar Projects and Inspirations
**Cloudflare Tunnels**:
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
**Authentik and Authelia**:
These projects inspired Pangolins centralized authentication system for proxies, enabling robust user and role management.
**Authelia**:
This inspired Pangolins centralized authentication system for proxies, enabling robust user and role management.
## Project Development / Roadmap
@@ -147,7 +143,7 @@ View the [project board](https://github.com/orgs/fosrl/projects/1) for more deta
## Licensing
Pangolin is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. Please see the [LICENSE](./LICENSE) file in the repository for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
## Contributions

View File

@@ -12,7 +12,7 @@ post {
body:json {
{
"email": "owen@fossorial.io",
"email": "admin@fosrl.io",
"password": "Password123!"
}
}

View File

@@ -0,0 +1,11 @@
meta {
name: adminListUsers
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/users
body: none
auth: none
}

View File

@@ -0,0 +1,11 @@
meta {
name: adminRemoveUser
type: http
seq: 3
}
delete {
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
body: none
auth: none
}

View File

@@ -1,9 +1,16 @@
# To see all available options, please visit the docs:
# https://docs.fossorial.io/Pangolin/Configuration/config
app:
dashboard_url: "http://localhost:3002"
base_domain: "localhost"
log_level: "info"
save_logs: false
domains:
domain1:
base_domain: "example.com"
cert_resolver: "letsencrypt"
server:
external_port: 3000
internal_port: 3001
@@ -11,10 +18,13 @@ server:
internal_hostname: "pangolin"
session_cookie_name: "p_session_token"
resource_access_token_param: "p_token"
secret: "your_secret_key_here"
resource_access_token_headers:
id: "P-Access-Token-Id"
token: "P-Access-Token"
resource_session_request_param: "p_session_request"
traefik:
cert_resolver: "letsencrypt"
http_entrypoint: "web"
https_entrypoint: "websecure"
@@ -29,7 +39,7 @@ gerbil:
rate_limits:
global:
window_minutes: 1
max_requests: 100
max_requests: 500
users:
server_admin:

View File

@@ -1,53 +0,0 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`{{.DashboardDomain}}`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# API router (handles /api/v1 paths)
api-router:
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# WebSocket router
ws-router:
rule: "Host(`{{.DashboardDomain}}`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:{{.NEXT_PORT}}" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:{{.EXTERNAL_PORT}}" # API/WebSocket server

View File

@@ -1,44 +0,0 @@
api:
insecure: true
dashboard: true
providers:
http:
endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.0.0-beta.3"
log:
level: "INFO"
format: "common"
certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
serversTransport:
insecureSkipVerify: true

View File

@@ -1,5 +1,4 @@
version: "3.7"
name: pangolin
services:
pangolin:
image: fosrl/pangolin:latest
@@ -11,7 +10,7 @@ services:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "3s"
timeout: "3s"
retries: 5
retries: 15
gerbil:
image: fosrl/gerbil:latest
@@ -32,12 +31,11 @@ services:
- SYS_MODULE
ports:
- 51820:51820/udp
- 8080:8080 # Port for traefik because of the network_mode
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
traefik:
image: traefik:v3.3.3
image: traefik:v3.4.0
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
@@ -47,8 +45,8 @@ services:
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
networks:
default:

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,10 +4,10 @@ import path from "path";
export default defineConfig({
dialect: "sqlite",
schema: path.join("server", "db", "schema.ts"),
schema: path.join("server", "db", "sqlite", "schema.ts"),
out: path.join("server", "migrations"),
verbose: true,
dbCredentials: {
url: path.join(APP_PATH, "db", "db.sqlite"),
},
url: path.join(APP_PATH, "db", "db.sqlite")
}
});

View File

@@ -52,6 +52,7 @@ esbuild
bundle: true,
outfile: argv.out,
format: "esm",
minify: true,
banner: {
js: banner,
},

View File

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

View File

@@ -1,13 +1,24 @@
all: build
all: update-versions go-build-release put-back
build:
CGO_ENABLED=0 go build -o bin/installer
release:
go-build-release:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64
clean:
rm -f bin/installer
rm -f bin/installer_linux_amd64
rm -f bin/installer_linux_arm64
update-versions:
@echo "Fetching latest versions..."
cp main.go main.go.bak && \
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$$PANGOLIN_VERSION\"/" main.go && \
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$$GERBIL_VERSION\"/" main.go && \
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
echo "Updated main.go with latest versions"
put-back:
mv main.go.bak main.go

353
install/config.go Normal file
View File

@@ -0,0 +1,353 @@
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"gopkg.in/yaml.v3"
)
// TraefikConfig represents the structure of the main Traefik configuration
type TraefikConfig struct {
Experimental struct {
Plugins struct {
Badger struct {
Version string `yaml:"version"`
} `yaml:"badger"`
} `yaml:"plugins"`
} `yaml:"experimental"`
CertificatesResolvers struct {
LetsEncrypt struct {
Acme struct {
Email string `yaml:"email"`
} `yaml:"acme"`
} `yaml:"letsencrypt"`
} `yaml:"certificatesResolvers"`
}
// DynamicConfig represents the structure of the dynamic configuration
type DynamicConfig struct {
HTTP struct {
Routers map[string]struct {
Rule string `yaml:"rule"`
} `yaml:"routers"`
} `yaml:"http"`
}
// ConfigValues holds the extracted configuration values
type ConfigValues struct {
DashboardDomain string
LetsEncryptEmail string
BadgerVersion string
}
// ReadTraefikConfig reads and extracts values from Traefik configuration files
func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) {
// Read main config file
mainConfigData, err := os.ReadFile(mainConfigPath)
if err != nil {
return nil, fmt.Errorf("error reading main config file: %w", err)
}
var mainConfig TraefikConfig
if err := yaml.Unmarshal(mainConfigData, &mainConfig); err != nil {
return nil, fmt.Errorf("error parsing main config file: %w", err)
}
// Read dynamic config file
dynamicConfigData, err := os.ReadFile(dynamicConfigPath)
if err != nil {
return nil, fmt.Errorf("error reading dynamic config file: %w", err)
}
var dynamicConfig DynamicConfig
if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil {
return nil, fmt.Errorf("error parsing dynamic config file: %w", err)
}
// Extract values
values := &ConfigValues{
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
}
// Extract DashboardDomain from router rules
// Look for it in the main router rules
for _, router := range dynamicConfig.HTTP.Routers {
if router.Rule != "" {
// Extract domain from Host(`mydomain.com`)
if domain := extractDomainFromRule(router.Rule); domain != "" {
values.DashboardDomain = domain
break
}
}
}
return values, nil
}
// extractDomainFromRule extracts the domain from a router rule
func extractDomainFromRule(rule string) string {
// Look for the Host(`mydomain.com`) pattern
if start := findPattern(rule, "Host(`"); start != -1 {
end := findPattern(rule[start:], "`)")
if end != -1 {
return rule[start+6 : start+end]
}
}
return ""
}
// findPattern finds the start of a pattern in a string
func findPattern(s, pattern string) int {
return bytes.Index([]byte(s), []byte(pattern))
}
func copyDockerService(sourceFile, destFile, serviceName string) error {
// Read source file
sourceData, err := os.ReadFile(sourceFile)
if err != nil {
return fmt.Errorf("error reading source file: %w", err)
}
// Read destination file
destData, err := os.ReadFile(destFile)
if err != nil {
return fmt.Errorf("error reading destination file: %w", err)
}
// Parse source Docker Compose YAML
var sourceCompose map[string]interface{}
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
}
// Parse destination Docker Compose YAML
var destCompose map[string]interface{}
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
}
// Get services section from source
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
if !ok {
return fmt.Errorf("services section not found in source file or has invalid format")
}
// Get the specific service configuration
serviceConfig, ok := sourceServices[serviceName]
if !ok {
return fmt.Errorf("service '%s' not found in source file", serviceName)
}
// Get or create services section in destination
destServices, ok := destCompose["services"].(map[string]interface{})
if !ok {
// If services section doesn't exist, create it
destServices = make(map[string]interface{})
destCompose["services"] = destServices
}
// Update service in destination
destServices[serviceName] = serviceConfig
// Marshal updated destination YAML
// Use yaml.v3 encoder to preserve formatting and comments
// updatedData, err := yaml.Marshal(destCompose)
updatedData, err := MarshalYAMLWithIndent(destCompose, 2)
if err != nil {
return fmt.Errorf("error marshaling updated Docker Compose file: %w", err)
}
// Write updated YAML back to destination file
if err := os.WriteFile(destFile, updatedData, 0644); err != nil {
return fmt.Errorf("error writing to destination file: %w", err)
}
return nil
}
func backupConfig() error {
// Backup docker-compose.yml
if _, err := os.Stat("docker-compose.yml"); err == nil {
if err := copyFile("docker-compose.yml", "docker-compose.yml.backup"); err != nil {
return fmt.Errorf("failed to backup docker-compose.yml: %v", err)
}
}
// Backup config directory
if _, err := os.Stat("config"); err == nil {
cmd := exec.Command("tar", "-czvf", "config.tar.gz", "config")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to backup config directory: %v", err)
}
}
return nil
}
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
buffer := new(bytes.Buffer)
encoder := yaml.NewEncoder(buffer)
encoder.SetIndent(indent)
err := encoder.Encode(data)
if err != nil {
return nil, err
}
defer encoder.Close()
return buffer.Bytes(), nil
}
func replaceInFile(filepath, oldStr, newStr string) error {
// Read the file content
content, err := os.ReadFile(filepath)
if err != nil {
return fmt.Errorf("error reading file: %v", err)
}
// Replace the string
newContent := strings.Replace(string(content), oldStr, newStr, -1)
// Write the modified content back to the file
err = os.WriteFile(filepath, []byte(newContent), 0644)
if err != nil {
return fmt.Errorf("error writing file: %v", err)
}
return nil
}
func CheckAndAddTraefikLogVolume(composePath string) error {
// Read the docker-compose.yml file
data, err := os.ReadFile(composePath)
if err != nil {
return fmt.Errorf("error reading compose file: %w", err)
}
// Parse YAML into a generic map
var compose map[string]interface{}
if err := yaml.Unmarshal(data, &compose); err != nil {
return fmt.Errorf("error parsing compose file: %w", err)
}
// Get services section
services, ok := compose["services"].(map[string]interface{})
if !ok {
return fmt.Errorf("services section not found or invalid")
}
// Get traefik service
traefik, ok := services["traefik"].(map[string]interface{})
if !ok {
return fmt.Errorf("traefik service not found or invalid")
}
// Check volumes
logVolume := "./config/traefik/logs:/var/log/traefik"
var volumes []interface{}
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok {
// Check if volume already exists
for _, v := range existingVolumes {
if v.(string) == logVolume {
fmt.Println("Traefik log volume is already configured")
return nil
}
}
volumes = existingVolumes
}
// Add new volume
volumes = append(volumes, logVolume)
traefik["volumes"] = volumes
// Write updated config back to file
newData, err := MarshalYAMLWithIndent(compose, 2)
if err != nil {
return fmt.Errorf("error marshaling updated compose file: %w", err)
}
if err := os.WriteFile(composePath, newData, 0644); err != nil {
return fmt.Errorf("error writing updated compose file: %w", err)
}
fmt.Println("Added traefik log volume and created logs directory")
return nil
}
// MergeYAML merges two YAML files, where the contents of the second file
// are merged into the first file. In case of conflicts, values from the
// second file take precedence.
func MergeYAML(baseFile, overlayFile string) error {
// Read the base YAML file
baseContent, err := os.ReadFile(baseFile)
if err != nil {
return fmt.Errorf("error reading base file: %v", err)
}
// Read the overlay YAML file
overlayContent, err := os.ReadFile(overlayFile)
if err != nil {
return fmt.Errorf("error reading overlay file: %v", err)
}
// Parse base YAML into a map
var baseMap map[string]interface{}
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
return fmt.Errorf("error parsing base YAML: %v", err)
}
// Parse overlay YAML into a map
var overlayMap map[string]interface{}
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
return fmt.Errorf("error parsing overlay YAML: %v", err)
}
// Merge the overlay into the base
merged := mergeMap(baseMap, overlayMap)
// Marshal the merged result back to YAML
mergedContent, err := MarshalYAMLWithIndent(merged, 2)
if err != nil {
return fmt.Errorf("error marshaling merged YAML: %v", err)
}
// Write the merged content back to the base file
if err := os.WriteFile(baseFile, mergedContent, 0644); err != nil {
return fmt.Errorf("error writing merged YAML: %v", err)
}
return nil
}
// mergeMap recursively merges two maps
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
// Copy all key-values from base map
for k, v := range base {
result[k] = v
}
// Merge overlay values
for k, v := range overlay {
// If both maps have the same key and both values are maps, merge recursively
if baseVal, ok := base[k]; ok {
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
result[k] = mergeMap(baseMap, overlayMap)
continue
}
}
}
// Otherwise, overlay value takes precedence
result[k] = v
}
return result
}

View File

@@ -1,9 +1,16 @@
# To see all available options, please visit the docs:
# https://docs.fossorial.io/Pangolin/Configuration/config
app:
dashboard_url: "https://{{.DashboardDomain}}"
base_domain: "{{.BaseDomain}}"
log_level: "info"
save_logs: false
domains:
domain1:
base_domain: "{{.BaseDomain}}"
cert_resolver: "letsencrypt"
server:
external_port: 3000
internal_port: 3001
@@ -11,18 +18,21 @@ server:
internal_hostname: "pangolin"
session_cookie_name: "p_session_token"
resource_access_token_param: "p_token"
resource_access_token_headers:
id: "P-Access-Token-Id"
token: "P-Access-Token"
resource_session_request_param: "p_session_request"
secret: {{.Secret}}
cors:
origins: ["https://{{.DashboardDomain}}"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
headers: ["X-CSRF-Token", "Content-Type"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
traefik:
cert_resolver: "letsencrypt"
http_entrypoint: "web"
https_entrypoint: "websecure"
prefer_wildcard_cert: false
gerbil:
start_port: 51820
@@ -35,7 +45,7 @@ gerbil:
rate_limits:
global:
window_minutes: 1
max_requests: 100
max_requests: 500
{{if .EnableEmail}}
email:
smtp_host: "{{.EmailSMTPHost}}"

View File

@@ -0,0 +1,6 @@
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
name: myAppSecComponent
source: appsec
labels:
type: appsec

View File

@@ -0,0 +1,5 @@
poll_without_inotify: false
filenames:
- /var/log/traefik/*.log
labels:
type: traefik

View File

@@ -0,0 +1,27 @@
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
environment:
GID: "1000"
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
ENROLL_INSTANCE_NAME: "pangolin-crowdsec"
PARSERS: crowdsecurity/whitelists
ENROLL_TAGS: docker
healthcheck:
interval: 10s
retries: 15
timeout: 10s
test: ["CMD", "cscli", "capi", "status"]
labels:
- "traefik.enable=false" # Disable traefik for crowdsec
volumes:
# crowdsec container data
- ./config/crowdsec:/etc/crowdsec # crowdsec config
- ./config/crowdsec/db:/var/lib/crowdsec/data # crowdsec db
# log bind mounts into crowdsec
- ./config/traefik/logs:/var/log/traefik # traefik logs
ports:
- 6060:6060 # metrics endpoint for prometheus
restart: unless-stopped
command: -t # Add test config flag to verify configuration

View File

@@ -0,0 +1,109 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
default-whitelist: # Whitelist middleware for internal IPs
ipWhiteList: # Internal IP addresses
sourceRange: # Internal IP addresses
- "10.0.0.0/8" # Internal IP addresses
- "192.168.0.0/16" # Internal IP addresses
- "172.16.0.0/12" # Internal IP addresses
# Basic security headers
security-headers:
headers:
customResponseHeaders: # Custom response headers
Server: "" # Remove server header
X-Powered-By: "" # Remove powered by header
X-Forwarded-Proto: "https" # Set forwarded proto to https
sslProxyHeaders: # SSL proxy headers
X-Forwarded-Proto: "https" # Set forwarded proto to https
hostsProxyHeaders: # Hosts proxy headers
- "X-Forwarded-Host" # Set forwarded host
contentTypeNosniff: true # Prevent MIME sniffing
customFrameOptionsValue: "SAMEORIGIN" # Set frame options
referrerPolicy: "strict-origin-when-cross-origin" # Set referrer policy
forceSTSHeader: true # Force STS header
stsIncludeSubdomains: true # Include subdomains
stsSeconds: 63072000 # STS seconds
stsPreload: true # Preload STS
# CrowdSec configuration with proper IP forwarding
crowdsec:
plugin:
crowdsec:
enabled: true # Enable CrowdSec plugin
logLevel: INFO # Log level
updateIntervalSeconds: 15 # Update interval
updateMaxFailure: 0 # Update max failure
defaultDecisionSeconds: 15 # Default decision seconds
httpTimeoutSeconds: 10 # HTTP timeout
crowdsecMode: live # CrowdSec mode
crowdsecAppsecEnabled: true # Enable AppSec
crowdsecAppsecHost: crowdsec:7422 # CrowdSec IP address which you noted down later
crowdsecAppsecFailureBlock: true # Block on failure
crowdsecAppsecUnreachableBlock: true # Block on unreachable
crowdsecAppsecBodyLimit: 10485760
crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
crowdsecLapiHost: crowdsec:8080 # CrowdSec
crowdsecLapiScheme: http # CrowdSec API scheme
forwardedHeadersTrustedIPs: # Forwarded headers trusted IPs
- "0.0.0.0/0" # All IP addresses are trusted for forwarded headers (CHANGE MADE HERE)
clientTrustedIPs: # Client trusted IPs (CHANGE MADE HERE)
- "10.0.0.0/8" # Internal LAN IP addresses
- "172.16.0.0/12" # Internal LAN IP addresses
- "192.168.0.0/16" # Internal LAN IP addresses
- "100.89.137.0/20" # Internal LAN IP addresses
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`{{.DashboardDomain}}`)" # Dynamic Domain Name
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)" # Dynamic Domain Name
service: next-service
entryPoints:
- websecure
middlewares:
- security-headers # Add security headers middleware
tls:
certResolver: letsencrypt
# API router (handles /api/v1 paths)
api-router:
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)" # Dynamic Domain Name
service: api-service
entryPoints:
- websecure
middlewares:
- security-headers # Add security headers middleware
tls:
certResolver: letsencrypt
# WebSocket router
ws-router:
rule: "Host(`{{.DashboardDomain}}`)" # Dynamic Domain Name
service: api-service
entryPoints:
- websecure
middlewares:
- security-headers # Add security headers middleware
tls:
certResolver: letsencrypt
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server

View File

@@ -0,0 +1,25 @@
name: captcha_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
decisions:
- type: captcha
duration: 4h
on_success: break
---
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
on_success: break
---
name: default_range_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
- type: ban
duration: 4h
on_success: break

View File

@@ -0,0 +1,91 @@
api:
insecure: true
dashboard: true
providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "{{.BadgerVersion}}"
crowdsec: # CrowdSec plugin configuration added
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.4.2"
log:
level: "INFO"
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
filePath: "/var/log/traefik/access.log"
format: json
filters:
statusCodes:
- "200-299" # Success codes
- "400-499" # Client errors
- "500-599" # Server errors
retryAttempts: true
minDuration: "100ms" # Increased to focus on slower requests
bufferingSize: 100 # Add buffering for better performance
fields:
defaultMode: drop # Start with dropping all fields
names:
ClientAddr: keep # Keep client address for IP tracking
ClientHost: keep # Keep client host for IP tracking
RequestMethod: keep # Keep request method for tracking
RequestPath: keep # Keep request path for tracking
RequestProtocol: keep # Keep request protocol for tracking
DownstreamStatus: keep # Keep downstream status for tracking
DownstreamContentSize: keep # Keep downstream content size for tracking
Duration: keep # Keep request duration for tracking
ServiceName: keep # Keep service name for tracking
StartUTC: keep # Keep start time for tracking
TLSVersion: keep # Keep TLS version for tracking
TLSCipher: keep # Keep TLS cipher for tracking
RetryAttempts: keep # Keep retry attempts for tracking
headers:
defaultMode: drop # Start with dropping all headers
names:
User-Agent: keep # Keep user agent for tracking
X-Real-Ip: keep # Keep real IP for tracking
X-Forwarded-For: keep # Keep forwarded IP for tracking
X-Forwarded-Proto: keep # Keep forwarded protocol for tracking
Content-Type: keep # Keep content type for tracking
Authorization: redact # Redact sensitive information
Cookie: redact # Redact sensitive information
certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
middlewares:
- crowdsec@file
serversTransport:
insecureSkipVerify: true

View File

@@ -1,3 +1,4 @@
name: pangolin
services:
pangolin:
image: fosrl/pangolin:{{.PangolinVersion}}
@@ -7,10 +8,9 @@ services:
- ./config:/app/config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "3s"
timeout: "3s"
retries: 5
interval: "10s"
timeout: "10s"
retries: 15
{{if .InstallGerbil}}
gerbil:
image: fosrl/gerbil:{{.GerbilVersion}}
@@ -34,15 +34,13 @@ services:
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
{{end}}
traefik:
image: traefik:v3.3.3
image: traefik:v3.4.1
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}
network_mode: service:gerbil # Ports appear on the gerbil service
{{end}}
{{if not .InstallGerbil}}
{{end}}{{if not .InstallGerbil}}
ports:
- 443:443
- 80:80
@@ -55,8 +53,9 @@ services:
volumes:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
networks:
default:
driver: bridge
name: pangolin
name: pangolin

View File

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

201
install/crowdsec.go Normal file
View File

@@ -0,0 +1,201 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"strings"
"gopkg.in/yaml.v3"
)
func installCrowdsec(config Config) error {
if err := stopContainers(); err != nil {
return fmt.Errorf("failed to stop containers: %v", err)
}
// Run installation steps
if err := backupConfig(); err != nil {
return fmt.Errorf("backup failed: %v", err)
}
if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
}
os.MkdirAll("config/crowdsec/db", 0755)
os.MkdirAll("config/crowdsec/acquis.d", 0755)
os.MkdirAll("config/traefik/logs", 0755)
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
fmt.Printf("Error copying docker service: %v\n", err)
os.Exit(1)
}
if err := MergeYAML("config/traefik/traefik_config.yml", "config/crowdsec/traefik_config.yml"); err != nil {
fmt.Printf("Error copying entry points: %v\n", err)
os.Exit(1)
}
// delete the 2nd file
if err := os.Remove("config/crowdsec/traefik_config.yml"); err != nil {
fmt.Printf("Error removing file: %v\n", err)
os.Exit(1)
}
if err := MergeYAML("config/traefik/dynamic_config.yml", "config/crowdsec/dynamic_config.yml"); err != nil {
fmt.Printf("Error copying entry points: %v\n", err)
os.Exit(1)
}
// delete the 2nd file
if err := os.Remove("config/crowdsec/dynamic_config.yml"); err != nil {
fmt.Printf("Error removing file: %v\n", err)
os.Exit(1)
}
if err := os.Remove("config/crowdsec/docker-compose.yml"); err != nil {
fmt.Printf("Error removing file: %v\n", err)
os.Exit(1)
}
if err := CheckAndAddTraefikLogVolume("docker-compose.yml"); err != nil {
fmt.Printf("Error checking and adding Traefik log volume: %v\n", err)
os.Exit(1)
}
// check and add the service dependency of crowdsec to traefik
if err := CheckAndAddCrowdsecDependency("docker-compose.yml"); err != nil {
fmt.Printf("Error adding crowdsec dependency to traefik: %v\n", err)
os.Exit(1)
}
if err := startContainers(); err != nil {
return fmt.Errorf("failed to start containers: %v", err)
}
// get API key
apiKey, err := GetCrowdSecAPIKey()
if err != nil {
return fmt.Errorf("failed to get API key: %v", err)
}
config.TraefikBouncerKey = apiKey
if err := replaceInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK", config.TraefikBouncerKey); err != nil {
return fmt.Errorf("failed to replace bouncer key: %v", err)
}
if err := restartContainer("traefik"); err != nil {
return fmt.Errorf("failed to restart containers: %v", err)
}
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer")
}
return nil
}
func checkIsCrowdsecInstalledInCompose() bool {
// Read docker-compose.yml
content, err := os.ReadFile("docker-compose.yml")
if err != nil {
return false
}
// Check for crowdsec service
return bytes.Contains(content, []byte("crowdsec:"))
}
func GetCrowdSecAPIKey() (string, error) {
// First, ensure the container is running
if err := waitForContainer("crowdsec"); err != nil {
return "", fmt.Errorf("waiting for container: %w", err)
}
// Execute the command to get the API key
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("executing command: %w", err)
}
// Trim any whitespace from the output
apiKey := strings.TrimSpace(out.String())
if apiKey == "" {
return "", fmt.Errorf("empty API key returned")
}
return apiKey, nil
}
func checkIfTextInFile(file, text string) bool {
// Read file
content, err := os.ReadFile(file)
if err != nil {
return false
}
// Check for text
return bytes.Contains(content, []byte(text))
}
func CheckAndAddCrowdsecDependency(composePath string) error {
// Read the docker-compose.yml file
data, err := os.ReadFile(composePath)
if err != nil {
return fmt.Errorf("error reading compose file: %w", err)
}
// Parse YAML into a generic map
var compose map[string]interface{}
if err := yaml.Unmarshal(data, &compose); err != nil {
return fmt.Errorf("error parsing compose file: %w", err)
}
// Get services section
services, ok := compose["services"].(map[string]interface{})
if !ok {
return fmt.Errorf("services section not found or invalid")
}
// Get traefik service
traefik, ok := services["traefik"].(map[string]interface{})
if !ok {
return fmt.Errorf("traefik service not found or invalid")
}
// Get dependencies
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
if ok {
// Append the new block for crowdsec
dependsOn["crowdsec"] = map[string]interface{}{
"condition": "service_healthy",
}
} else {
// No dependencies exist, create it
traefik["depends_on"] = map[string]interface{}{
"crowdsec": map[string]interface{}{
"condition": "service_healthy",
},
}
}
// Marshal the modified data back to YAML with indentation
modifiedData, err := MarshalYAMLWithIndent(compose, 2) // Set indentation to 2 spaces
if err != nil {
log.Fatalf("error marshaling YAML: %v", err)
}
if err := os.WriteFile(composePath, modifiedData, 0644); err != nil {
return fmt.Errorf("error writing updated compose file: %w", err)
}
fmt.Println("Added dependency of crowdsec to traefik")
return nil
}

View File

@@ -3,6 +3,8 @@ module installer
go 1.23.0
require (
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/term v0.28.0
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.29.0 // indirect

View File

@@ -2,3 +2,7 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

12
install/input.txt Normal file
View File

@@ -0,0 +1,12 @@
example.com
pangolin.example.com
admin@example.com
yes
admin@example.com
Password123!
Password123!
yes
no
no
no
yes

View File

@@ -2,17 +2,23 @@ package main
import (
"bufio"
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"syscall"
"text/template"
"time"
"unicode"
"math/rand"
"strconv"
"golang.org/x/term"
)
@@ -24,7 +30,7 @@ func loadVersions(config *Config) {
config.BadgerVersion = "replaceme"
}
//go:embed fs/*
//go:embed config/*
var configFiles embed.FS
type Config struct {
@@ -45,40 +51,122 @@ type Config struct {
EmailSMTPPass string
EmailNoReply string
InstallGerbil bool
TraefikBouncerKey string
DoCrowdsecInstall bool
Secret string
}
func main() {
reader := bufio.NewReader(os.Stdin)
// check if the user is root
if os.Geteuid() != 0 {
fmt.Println("This script must be run as root")
// check if docker is not installed and the user is root
if !isDockerInstalled() {
if os.Geteuid() != 0 {
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)
}
var config Config
// check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil {
config := collectUserInput(reader)
config = collectUserInput(reader)
loadVersions(&config)
config.DoCrowdsecInstall = false
config.Secret = generateRandomSecretKey()
if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
}
moveFile("config/docker-compose.yml", "docker-compose.yml")
if !isDockerInstalled() && runtime.GOOS == "linux" {
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
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!")
}
}
fmt.Println("\n=== Starting installation ===")
if isDockerInstalled() {
if readBool(reader, "Would you like to install and start the containers?", true) {
if err := pullContainers(); err != nil {
fmt.Println("Error: ", err)
return
}
if err := startContainers(); err != nil {
fmt.Println("Error: ", err)
return
}
}
}
} else {
fmt.Println("Config file already exists... skipping configuration")
fmt.Println("Looks like you already installed, so I am going to do the setup...")
}
if isDockerInstalled() {
if readBool(reader, "Would you like to install and start the containers?", true) {
pullAndStartContainers()
if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== CrowdSec Install ===")
// check if crowdsec is installed
if readBool(reader, "Would you like to install CrowdSec?", false) {
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
if config.DashboardDomain == "" {
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
if err != nil {
fmt.Printf("Error reading config: %v\n", err)
return
}
config.DashboardDomain = traefikConfig.DashboardDomain
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
config.BadgerVersion = traefikConfig.BadgerVersion
// print the values and check if they are right
fmt.Println("Detected values:")
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
if !readBool(reader, "Are these values correct?", true) {
config = collectUserInput(reader)
}
}
config.DoCrowdsecInstall = true
installCrowdsec(config)
}
}
}
@@ -99,22 +187,24 @@ func readString(reader *bufio.Reader, prompt string, defaultValue string) string
return input
}
func readPassword(prompt string) string {
fmt.Print(prompt + ": ")
// Read password without echo
password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // Add a newline since ReadPassword doesn't add one
if err != nil {
return ""
func readPassword(prompt string, reader *bufio.Reader) string {
if term.IsTerminal(int(syscall.Stdin)) {
fmt.Print(prompt + ": ")
// Read password without echo if we're in a terminal
password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // Add a newline since ReadPassword doesn't add one
if err != nil {
return ""
}
input := strings.TrimSpace(string(password))
if input == "" {
return readPassword(prompt, reader)
}
return input
} else {
// Fallback to reading from stdin if not in a terminal
return readString(reader, prompt, "")
}
input := strings.TrimSpace(string(password))
if input == "" {
return readPassword(prompt)
}
return input
}
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
@@ -144,14 +234,14 @@ func collectUserInput(reader *bufio.Reader) Config {
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunned connections", true)
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
// Admin user configuration
fmt.Println("\n=== Admin User Configuration ===")
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
for {
pass1 := readPassword("Create admin user password")
pass2 := readPassword("Confirm admin user password")
pass1 := readPassword("Create admin user password", reader)
pass2 := readPassword("Confirm admin user password", reader)
if pass1 != pass2 {
fmt.Println("Passwords do not match")
@@ -261,31 +351,33 @@ func createConfigFiles(config Config) error {
os.MkdirAll("config/logs", 0755)
// Walk through all embedded files
err := fs.WalkDir(configFiles, "fs", func(path string, d fs.DirEntry, err error) error {
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip the root fs directory itself
if path == "fs" {
if path == "config" {
return nil
}
// Get the relative path by removing the "fs/" prefix
relPath := strings.TrimPrefix(path, "fs/")
if !config.DoCrowdsecInstall && strings.Contains(path, "crowdsec") {
return nil
}
// skip .DS_Store
if strings.Contains(relPath, ".DS_Store") {
return nil
}
if config.DoCrowdsecInstall && !strings.Contains(path, "crowdsec") {
return nil
}
// Create the full output path under "config/"
outPath := filepath.Join("config", relPath)
// skip .DS_Store
if strings.Contains(path, ".DS_Store") {
return nil
}
if d.IsDir() {
// Create directory
if err := os.MkdirAll(outPath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", outPath, err)
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", path, err)
}
return nil
}
@@ -303,14 +395,14 @@ func createConfigFiles(config Config) error {
}
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
return fmt.Errorf("failed to create parent directory for %s: %v", outPath, err)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("failed to create parent directory for %s: %v", path, err)
}
// Create output file
outFile, err := os.Create(outPath)
outFile, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create %s: %v", outPath, err)
return fmt.Errorf("failed to create %s: %v", path, err)
}
defer outFile.Close()
@@ -326,27 +418,6 @@ func createConfigFiles(config Config) error {
return fmt.Errorf("error walking config files: %v", err)
}
// get the current directory
dir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %v", err)
}
sourcePath := filepath.Join(dir, "config/docker-compose.yml")
destPath := filepath.Join(dir, "docker-compose.yml")
// Check if source file exists
if _, err := os.Stat(sourcePath); err != nil {
return fmt.Errorf("source docker-compose.yml not found: %v", err)
}
// Try to move the file
err = os.Rename(sourcePath, destPath)
if err != nil {
return fmt.Errorf("failed to move docker-compose.yml from %s to %s: %v",
sourcePath, destPath, err)
}
return nil
}
@@ -358,7 +429,7 @@ func installDocker() error {
return fmt.Errorf("failed to detect Linux distribution: %v", err)
}
osRelease := string(output)
// Detect system architecture
archCmd := exec.Command("uname", "-m")
archOutput, err := archCmd.Output()
@@ -366,7 +437,7 @@ func installDocker() error {
return fmt.Errorf("failed to detect system architecture: %v", err)
}
arch := strings.TrimSpace(string(archOutput))
// Map architecture to Docker's architecture naming
var dockerArch string
switch arch {
@@ -399,24 +470,44 @@ func installDocker() error {
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
`, dockerArch))
case strings.Contains(osRelease, "ID=fedora"):
// 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 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
`))
`, repoCmd))
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
installCmd = exec.Command("bash", "-c", `
zypper install -y docker docker-compose &&
systemctl enable docker
`)
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
installCmd = exec.Command("bash", "-c", `
dnf remove -y runc &&
dnf -y install yum-utils &&
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
systemctl enable docker
`))
`)
case strings.Contains(osRelease, "ID=amzn"):
installCmd = exec.Command("bash", "-c", `
yum update -y &&
@@ -427,11 +518,26 @@ func installDocker() error {
default:
return fmt.Errorf("unsupported Linux distribution")
}
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
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 {
cmd := exec.Command("docker", "--version")
if err := cmd.Run(); err != nil {
@@ -440,53 +546,181 @@ func isDockerInstalled() bool {
return true
}
func getCommandString(useNewStyle bool) string {
if useNewStyle {
return "'docker compose'"
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
}
return "'docker-compose'"
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
}
func pullAndStartContainers() error {
fmt.Println("Starting containers...")
// 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
}
// Check which docker compose command is available
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
func executeDockerComposeCommandWithArgs(args ...string) error {
var cmd *exec.Cmd
var useNewStyle bool
if !isDockerInstalled() {
return fmt.Errorf("docker is not installed")
}
checkCmd := exec.Command("docker", "compose", "version")
if err := checkCmd.Run(); err == nil {
useNewStyle = true
} else {
// Check if docker-compose (old style) is available
checkCmd = exec.Command("docker-compose", "version")
if err := checkCmd.Run(); err != nil {
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
}
}
// Helper function to execute docker compose commands
executeCommand := func(args ...string) error {
var cmd *exec.Cmd
if useNewStyle {
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
if err := checkCmd.Run(); err == nil {
useNewStyle = false
} else {
cmd = exec.Command("docker-compose", args...)
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
if useNewStyle {
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
} else {
cmd = exec.Command("docker-compose", args...)
}
// Pull containers
fmt.Printf("Using %s command to pull containers...\n", getCommandString(useNewStyle))
if err := executeCommand("-f", "docker-compose.yml", "pull"); err != nil {
return fmt.Errorf("failed to pull containers: %v", err)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// pullContainers pulls the containers using the appropriate command.
func pullContainers() error {
fmt.Println("Pulling the container images...")
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
return fmt.Errorf("failed to pull the containers: %v", err)
}
// Start containers
fmt.Printf("Using %s command to start containers...\n", getCommandString(useNewStyle))
if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil {
return nil
}
// startContainers starts the containers using the appropriate command.
func startContainers() error {
fmt.Println("Starting containers...")
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
return fmt.Errorf("failed to start containers: %v", err)
}
return nil
}
// stopContainers stops the containers using the appropriate command.
func stopContainers() error {
fmt.Println("Stopping containers...")
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
return fmt.Errorf("failed to stop containers: %v", err)
}
return nil
}
// restartContainer restarts a specific container using the appropriate command.
func restartContainer(container string) error {
fmt.Println("Restarting containers...")
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
}
return nil
}
func copyFile(src, dst string) error {
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
_, err = io.Copy(destination, source)
return err
}
func moveFile(src, dst string) error {
if err := copyFile(src, dst); err != nil {
return err
}
return os.Remove(src)
}
func waitForContainer(containerName string) error {
maxAttempts := 30
retryInterval := time.Second * 2
for attempt := 0; attempt < maxAttempts; attempt++ {
// Check if container is running
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
// If the container doesn't exist or there's another error, wait and retry
time.Sleep(retryInterval)
continue
}
isRunning := strings.TrimSpace(out.String()) == "true"
if isRunning {
return nil
}
// Container exists but isn't running yet, wait and retry
time.Sleep(retryInterval)
}
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
}
func generateRandomSecretKey() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const length = 32
var seededRand *rand.Rand = rand.New(
rand.NewSource(time.Now().UnixNano()))
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}

View File

@@ -1,3 +1,23 @@
## Authentication Site
| EN | DE | Notes |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Bereitgestellt von [Pangolin](https://github.com/fosrl/pangolin) | |
| Authentication Required | Authentifizierung erforderlich | |
| Choose your preferred method to access {resource} | Wählen Sie Ihre bevorzugte Methode, um auf {resource} zuzugreifen | |
| PIN | PIN | |
| User | Benutzer | |
| 6-digit PIN Code | 6-stelliger PIN-Code | pin login |
| Login in with PIN | Mit PIN anmelden | pin login |
| Email | E-Mail | user login |
| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | user login |
| Password | Passwort | user login |
| Enter your password | Geben Sie Ihr Passwort ein | user login |
| Forgot your password? | Passwort vergessen? | user login |
| Log in | Anmelden | user login |
---
## Login site
| EN | DE | Notes |

291
internationalization/es.md Normal file
View File

@@ -0,0 +1,291 @@
## Authentication Site
| EN | ES | Notes |
| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- |
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Desarrollado por [Pangolin](https://github.com/fosrl/pangolin) | |
| Authentication Required | Se requiere autenticación | |
| Choose your preferred method to access {resource} | Elije tu método requerido para acceder a {resource} | |
| PIN | PIN | |
| User | Usuario | |
| 6-digit PIN Code | Código PIN de 6 dígitos | pin login |
| Login in with PIN | Registrate con PIN | pin login |
| Email | Email | user login |
| Enter your email | Introduce tu email | user login |
| Password | Contraseña | user login |
| Enter your password | Introduce tu contraseña | user login |
| Forgot your password? | ¿Olvidaste tu contraseña? | user login |
| Log in | Iniciar sesión | user login |
## Login site
| EN | ES | Notes |
| --------------------- | ---------------------------------- | ----------- |
| Welcome to Pangolin | Binvenido a Pangolin | |
| Log in to get started | Registrate para comenzar | |
| Email | Email | |
| Enter your email | Introduce tu email | placeholder |
| Password | Contraseña | |
| Enter your password | Introduce tu contraseña | placeholder |
| Forgot your password? | ¿Olvidaste tu contraseña? | |
| Log in | Iniciar sesión | |
# Ogranization site after successful login
| EN | ES | Notes |
| ----------------------------------------- | -------------------------------------------- | ----- |
| Welcome to Pangolin | Binvenido a Pangolin | |
| You're a member of {number} organization. | Eres miembro de la organización {number}. | |
## Shared Header, Navbar and Footer
##### Header
| EN | ES | Notes |
| ------------------- | ------------------- | ----- |
| Documentation | Documentación | |
| Support | Soporte | |
| Organization {name} | Organización {name} | |
##### Organization selector
| EN | ES | Notes |
| ---------------- | ----------------- | ----- |
| Search… | Buscar… | |
| Create | Crear | |
| New Organization | Nueva Organización| |
| Organizations | Organizaciones | |
##### Navbar
| EN | ES | Notes |
| --------------- | -----------------------| ----- |
| Sites | Sitios | |
| Resources | Recursos | |
| User & Roles | Usuarios y roles | |
| Shareable Links | Enlaces para compartir | |
| General | General | |
##### Footer
| EN | ES | |
| ------------------------- | --------------------------- | -------|
| Page {number} of {number} | Página {number} de {number} | footer |
| Rows per page | Filas por página | footer |
| Pangolin | Pangolin | footer |
| Built by Fossorial | Construido por Fossorial | footer |
| Open Source | Código abierto | footer |
| Documentation | Documentación | footer |
| {version} | {version} | footer |
## Main “Sites”
##### “Hero” section
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
| Newt (Recommended) | Newt (Recomendado) | |
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Para obtener la mejor experiencia de usuario, utiliza Newt. Utiliza WireGuard internamente y te permite abordar tus recursos privados mediante tu dirección LAN en tu red privada desde el panel de Pangolin. | |
| Runs in Docker | Se ejecuta en Docker | |
| Runs in shell on macOS, Linux, and Windows | Se ejecuta en shell en macOS, Linux y Windows | |
| Install Newt | Instalar Newt | |
| Basic WireGuard<br> | WireGuard básico<br> | |
| Compatible with all WireGuard clients<br> | Compatible con todos los clientes WireGuard<br> | |
| Manual configuration required | Se requiere configuración manual | |
##### Content
| EN | ES | Notes |
| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
| Manage Sites | Administrar sitios | |
| Allow connectivity to your network through secure tunnels | Permitir la conectividad a tu red a través de túneles seguros| |
| Search sites | Buscar sitios | placeholder |
| Add Site | Agregar sitio | |
| Name | Nombre | table header |
| Online | Conectado | table header |
| Site | Sitio | table header |
| Data In | Datos en | table header |
| Data Out | Datos de salida | table header |
| Connection Type | Tipo de conexión | table header |
| Online | Conectado | site state |
| Offline | Desconectado | site state |
| Edit → | Editar → | |
| View settings | Ver configuración | Popup after clicking “…” on site |
| Delete | Borrar | Popup after clicking “…” on site |
##### Add Site Popup
| EN | ES | Notes |
| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- |
| Create Site | Crear sitio | |
| Create a new site to start connection for this site | Crear un nuevo sitio para iniciar la conexión para este sitio | |
| Name | Nombre | |
| Site name | Nombre del sitio | placeholder |
| This is the name that will be displayed for this site. | Este es el nombre que se mostrará para este sitio. | desc |
| Method | Método | |
| Local | Local | |
| Newt | Newt | |
| WireGuard | WireGuard | |
| This is how you will expose connections. | Así es como expondrás las conexiones. | |
| You will only be able to see the configuration once. | Solo podrás ver la configuración una vez. | |
| Learn how to install Newt on your system | Aprende a instalar Newt en tu sistema | |
| I have copied the config | He copiado la configuración | |
| Create Site | Crear sitio | |
| Close | Cerrar | |
## Main “Resources”
##### “Hero” section
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
| Resources | Recursos | |
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. |Los recursos son servidores proxy para aplicaciones que se ejecutan en su red privada. Cree un recurso para cada aplicación HTTP o HTTPS en su red privada. Cada recurso debe estar conectado a un sitio web para proporcionar una conexión privada y segura a través del túnel cifrado WireGuard. | |
| Secure connectivity with WireGuard encryption | Conectividad segura con encriptación WireGuard | |
| Configure multiple authentication methods | Configura múltiples métodos de autenticación | |
| User and role-based access control | Control de acceso basado en usuarios y roles | |
##### Content
| EN | ES | Notes |
| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- |
| Manage Resources | Administrar recursos | |
| Create secure proxies to your private applications | Crea servidores proxy seguros para tus aplicaciones privadas | |
| Search resources | Buscar recursos | placeholder |
| Name | Nombre | |
| Site | Sitio | |
| Full URL | URL completa | |
| Authentication | Autenticación | |
| Not Protected | No protegido | authentication state |
| Protected | Protegido | authentication state |
| Edit → | Editar → | |
| Add Resource | Agregar recurso | |
##### Add Resource Popup
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- |
| Create Resource | Crear recurso | |
| Create a new resource to proxy request to your app | Crea un nuevo recurso para enviar solicitudes a tu aplicación | |
| Name | Nombre | |
| My Resource | Mi recurso | name placeholder |
| This is the name that will be displayed for this resource. | Este es el nombre que se mostrará para este recurso. | |
| Subdomain | Subdominio | |
| Enter subdomain | Ingresar subdominio | |
| This is the fully qualified domain name that will be used to access the resource. | Este es el nombre de dominio completo que se utilizará para acceder al recurso. | |
| Site | Sitio | |
| Search site… | Buscar sitio… | Site selector popup |
| This is the site that will be used in the dashboard. | Este es el sitio que se utilizará en el panel de control. | |
| Create Resource | Crear recurso | |
| Close | Cerrar | |
## Main “User & Roles”
##### Content
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
| Manage User & Roles | Administrar usuarios y roles | |
| Invite users and add them to roles to manage access to your organization | Invita a usuarios y agrégalos a roles para administrar el acceso a tu organización | |
| Users | Usuarios | sidebar item |
| Roles | Roles | sidebar item |
| **User tab** | **Pestaña de usuario** | |
| Search users | Buscar usuarios | placeholder |
| Invite User | Invitar usuario | addbutton |
| Email | Email | table header |
| Status | Estado | table header |
| Role | Role | table header |
| Confirmed | Confirmado | account status |
| Not confirmed (?) | No confirmado (?) | unknown for me account status |
| Owner | Dueño | role |
| Admin | Administrador | role |
| Member | Miembro | role |
| **Roles Tab** | **Pestaña Roles** | |
| Search roles | Buscar roles | placeholder |
| Add Role | Agregar rol | addbutton |
| Name | Nombre | table header |
| Description | Descripción | table header |
| Admin | Administrador | role |
| Member | Miembro | role |
| Admin role with the most permissions | Rol de administrador con más permisos | admin role desc |
| Members can only view resources | Los miembros sólo pueden ver los recursos | member role desc |
##### Invite User popup
| EN | ES | Notes |
| ----------------- | ------------------------------------------------------- | ----------- |
| Invite User | Invitar usuario | |
| Email | Email | |
| Enter an email | Introduzca un email | placeholder |
| Role | Rol | |
| Select role | Seleccionar rol | placeholder |
| Gültig für | Válido para | |
| 1 day | 1 día | |
| 2 days | 2 días | |
| 3 days | 3 días | |
| 4 days | 4 días | |
| 5 days | 5 días | |
| 6 days | 6 días | |
| 7 days | 7 días | |
| Create Invitation | Crear invitación | |
| Close | Cerrar | |
## Main “Shareable Links”
##### “Hero” section
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
| Shareable Links | Enlaces para compartir | |
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Crear enlaces que se puedan compartir a tus recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puedes configurar la duración de caducidad del enlace cuando lo creas. | |
| Easy to create and share | Fácil de crear y compartir | |
| Configurable expiration duration | Duración de expiración configurable | |
| Secure and revocable | Seguro y revocable | |
##### Content
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
| Manage Shareable Links | Administrar enlaces compartibles | |
| Create shareable links to grant temporary or permanent access to your resources | Crear enlaces compartibles para otorgar acceso temporal o permanente a tus recursos | |
| Search links | Buscar enlaces | placeholder |
| Create Share Link | Crear enlace para compartir | addbutton |
| Resource | Recurso | table header |
| Title | Título | table header |
| Created | Creado | table header |
| Expires | Caduca | table header |
| No links. Create one to get started. | No hay enlaces. Crea uno para comenzar. | table placeholder |
##### Create Shareable Link popup
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- |
| Create Shareable Link | Crear un enlace para compartir | |
| Anyone with this link can access the resource | Cualquier persona con este enlace puede acceder al recurso. | |
| Resource | Recurso | |
| Select resource | Seleccionar recurso | |
| Search resources… | Buscar recursos… | resource selector popup |
| Title (optional) | Título (opcional) | |
| Enter title | Introducir título | placeholder |
| Expire in | Caduca en | |
| Minutes | Minutos | |
| Hours | Horas | |
| Days | Días | |
| Months | Meses | |
| Years | Años | |
| Never expire | Nunca caduca | |
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | El tiempo de expiración es el tiempo durante el cual el enlace se podrá utilizar y brindará acceso al recurso. Después de este tiempo, el enlace dejará de funcionar y los usuarios que lo hayan utilizado perderán el acceso al recurso. | |
| Create Link | Crear enlace | |
| Close | Cerrar | |
## Main “General”
| EN | ES | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ |
| General | General | |
| Configure your organizations general settings | Configura los ajustes generales de tu organización | |
| General | General | sidebar item |
| Organization Settings | Configuración de la organización | |
| Manage your organization details and configuration | Administra los detalles y la configuración de tu organización| |
| Name | Nombre | |
| This is the display name of the org | Este es el nombre para mostrar de la organización. | |
| Save Settings | Guardar configuración | |
| Danger Zone | Zona de peligro | |
| Once you delete this org, there is no going back. Please be certain. | Una vez que elimines esta organización, no habrá vuelta atrás. Asegúrate de hacerlo. | |
| Delete Organization Data | Eliminar datos de la organización | |

310
internationalization/tr.md Normal file
View File

@@ -0,0 +1,310 @@
## Authentication Site
| EN | TR | Notes |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Pangolin Tarafından Destekleniyor | |
| Authentication Required | Kimlik Doğrulaması Gerekli | |
| Choose your preferred method to access {resource} | {resource}'a erişmek için tercih ettiğiniz yöntemi seçin | |
| PIN | PIN | |
| User | Kullanıcı | |
| 6-digit PIN Code | 6 haneli PIN Kodu | pin login |
| Login in with PIN | PIN ile Giriş Yap | pin login |
| Email | E-posta | user login |
| Enter your email | E-postanızı girin | user login |
| Password | Şifre | user login |
| Enter your password | Şifrenizi girin | user login |
| Forgot your password? | Şifrenizi mi unuttunuz? | user login |
| Log in | Giriş Yap | user login |
---
## Login site
| EN | TR | Notes |
| --------------------- | ------------------------------------------------------ | ----------- |
| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | |
| Log in to get started | Başlamak için giriş yapın | |
| Email | E-posta | |
| Enter your email | E-posta adresinizi girin | placeholder |
| Password | Şifre | |
| Enter your password | Şifrenizi girin | placeholder |
| Forgot your password? | Şifrenizi mi unuttunuz? | |
| Log in | Giriş Yap | |
---
# Organization site after successful login
| EN | TR | Notes |
| ----------------------------------------- | ------------------------------------------------------------------- | ----- |
| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | |
| You're a member of {number} organization. | {number} organizasyonunun üyesiniz. | |
---
## Shared Header, Navbar and Footer
##### Header
| EN | TR | Notes |
| ------------------- | -------------------------- | ----- |
| Documentation | Dokümantasyon | |
| Support | Destek | |
| Organization {name} | Organizasyon {name} | |
##### Organization selector
| EN | TR | Notes |
| ---------------- | ---------------------- | ----- |
| Search… | Ara… | |
| Create | Oluştur | |
| New Organization | Yeni Organizasyon | |
| Organizations | Organizasyonlar | |
##### Navbar
| EN | TR | Notes |
| --------------- | ------------------------------- | ----- |
| Sites | Siteler | |
| Resources | Kaynaklar | |
| User & Roles | Kullanıcılar ve Roller | |
| Shareable Links | Paylaşılabilir Linkler | |
| General | Genel | |
##### Footer
| EN | TR | Notes |
| ------------------------- | ------------------------------------------------ | -------------------- |
| Page {number} of {number} | Sayfa {number} / {number} | |
| Rows per page | Sayfa başına satırlar | |
| Pangolin | Pangolin | Footer'da yer alır |
| Built by Fossorial | Fossorial tarafından oluşturuldu | Footer'da yer alır |
| Open Source | Açık Kaynak | Footer'da yer alır |
| Documentation | Dokümantasyon | Footer'da yer alır |
| {version} | {version} | Footer'da yer alır |
---
## Main “Sites”
##### “Hero” section
| EN | TR | Notes |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----- |
| Newt (Recommended) | Newt (Tavsiye Edilen) | |
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | En iyi kullanıcı deneyimi için Newt'i kullanın. Newt, arka planda WireGuard kullanır ve Pangolin kontrol paneli üzerinden özel ağınızdaki kaynaklarınıza LAN adresleriyle erişmenizi sağlar. | |
| Runs in Docker | Docker üzerinde çalışır | |
| Runs in shell on macOS, Linux, and Windows | macOS, Linux ve Windowsta komut satırında çalışır | |
| Install Newt | Newt'i Yükle | |
| Basic WireGuard<br> | Temel WireGuard<br> | |
| Compatible with all WireGuard clients<br> | Tüm WireGuard istemcileriyle uyumlu<br> | |
| Manual configuration required | Manuel yapılandırma gereklidir | |
##### Content
| EN | TR | Notes |
| --------------------------------------------------------- | --------------------------------------------------------------------------- | ------------ |
| Manage Sites | Siteleri Yönet | |
| Allow connectivity to your network through secure tunnels | Güvenli tüneller aracılığıyla ağınıza bağlantı sağlayın | |
| Search sites | Siteleri ara | placeholder |
| Add Site | Site Ekle | |
| Name | Ad | Table Header |
| Online | Çevrimiçi | Table Header |
| Site | Site | Table Header |
| Data In | Gelen Veri | Table Header |
| Data Out | Giden Veri | Table Header |
| Connection Type | Bağlantı Türü | Table Header |
| Online | Çevrimiçi | Site state |
| Offline | Çevrimdışı | Site state |
| Edit → | Düzenle → | |
| View settings | Ayarları Görüntüle | Popup |
| Delete | Sil | Popup |
##### Add Site Popup
| EN | TR | Notes |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ----------- |
| Create Site | Site Oluştur | |
| Create a new site to start connection for this site | Bu site için bağlantıyı başlatmak amacıyla yeni bir site oluşturun | |
| Name | Ad | |
| Site name | Site adı | placeholder |
| This is the name that will be displayed for this site. | Bu, site için görüntülenecek addır. | desc |
| Method | Yöntem | |
| Local | Yerel | |
| Newt | Newt | |
| WireGuard | WireGuard | |
| This is how you will expose connections. | Bağlantılarınızı bu şekilde açığa çıkaracaksınız. | |
| You will only be able to see the configuration once. | Yapılandırmayı yalnızca bir kez görüntüleyebilirsiniz. | |
| Learn how to install Newt on your system | Sisteminizde Newt'in nasıl kurulacağını öğrenin | |
| I have copied the config | Yapılandırmayı kopyaladım | |
| Create Site | Site Oluştur | |
| Close | Kapat | |
---
## Main “Resources”
##### “Hero” section
| EN | TR | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----- |
| Resources | Kaynaklar | |
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Kaynaklar, özel ağınızda çalışan uygulamalar için proxy sunucularıdır. Özel ağınızdaki her HTTP veya HTTPS uygulaması için bir kaynak oluşturun. Her kaynağın, şifrelenmiş WireGuard tüneli üzerinden özel ve güvenli bağlantı sağlamak üzere bir siteyle ilişkili olması gerekir. | |
| Secure connectivity with WireGuard encryption | WireGuard şifrelemesiyle güvenli bağlantı | |
| Configure multiple authentication methods | Birden çok kimlik doğrulama yöntemini yapılandırın | |
| User and role-based access control | Kullanıcı ve role dayalı erişim kontrolü | |
##### Content
| EN | TR | Notes |
| -------------------------------------------------- | ------------------------------------------------------------- | -------------------- |
| Manage Resources | Kaynakları Yönet | |
| Create secure proxies to your private applications | Özel uygulamalarınız için güvenli proxyler oluşturun | |
| Search resources | Kaynakları ara | placeholder |
| Name | Ad | |
| Site | Site | |
| Full URL | Tam URL | |
| Authentication | Kimlik Doğrulama | |
| Not Protected | Korunmayan | authentication state |
| Protected | Korunan | authentication state |
| Edit → | Düzenle → | |
| Add Resource | Kaynak Ekle | |
##### Add Resource Popup
| EN | TR | Notes |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------- |
| Create Resource | Kaynak Oluştur | |
| Create a new resource to proxy request to your app | Uygulamanıza gelen istekleri yönlendirmek için yeni bir kaynak oluşturun | |
| Name | Ad | |
| My Resource | Kaynağım | name placeholder |
| This is the name that will be displayed for this resource. | Bu, kaynağın görüntülenecek adıdır. | |
| Subdomain | Alt alan adı | |
| Enter subdomain | Alt alan adını girin | |
| This is the fully qualified domain name that will be used to access the resource. | Kaynağa erişmek için kullanılacak tam nitelikli alan adıdır. | |
| Site | Site | |
| Search site… | Site ara… | Site selector popup |
| This is the site that will be used in the dashboard. | Kontrol panelinde kullanılacak sitedir. | |
| Create Resource | Kaynak Oluştur | |
| Close | Kapat | |
---
## Main “User & Roles”
##### Content
| EN | TR | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------- |
| Manage User & Roles | Kullanıcılar ve Rolleri Yönet | |
| Invite users and add them to roles to manage access to your organization | Organizasyonunuza erişimi yönetmek için kullanıcıları davet edin ve rollere atayın | |
| Users | Kullanıcılar | sidebar item |
| Roles | Roller | sidebar item |
| **User tab** | **Kullanıcı Sekmesi** | |
| Search users | Kullanıcıları ara | placeholder |
| Invite User | Kullanıcı Davet Et | addbutton |
| Email | E-posta | table header |
| Status | Durum | table header |
| Role | Rol | table header |
| Confirmed | Onaylandı | account status |
| Not confirmed (?) | Onaylanmadı (?) | account status |
| Owner | Sahip | role |
| Admin | Yönetici | role |
| Member | Üye | role |
| **Roles Tab** | **Roller Sekmesi** | |
| Search roles | Rolleri ara | placeholder |
| Add Role | Rol Ekle | addbutton |
| Name | Ad | table header |
| Description | Açıklama | table header |
| Admin | Yönetici | role |
| Member | Üye | role |
| Admin role with the most permissions | En fazla yetkiye sahip yönetici rolü | admin role desc |
| Members can only view resources | Üyeler yalnızca kaynakları görüntüleyebilir | member role desc |
##### Invite User popup
| EN | TR | Notes |
| ----------------- | ----------------------------------------------------------------------- | ----------- |
| Invite User | Kullanıcı Davet Et | |
| Email | E-posta | |
| Enter an email | Bir e-posta adresi girin | placeholder |
| Role | Rol | |
| Select role | Rol seçin | placeholder |
| Gültig für | Geçerlilik Süresi | |
| 1 day | 1 gün | |
| 2 days | 2 gün | |
| 3 days | 3 gün | |
| 4 days | 4 gün | |
| 5 days | 5 gün | |
| 6 days | 6 gün | |
| 7 days | 7 gün | |
| Create Invitation | Davetiye Oluştur | |
| Close | Kapat | |
---
## Main “Shareable Links”
##### “Hero” section
| EN | TR | Notes |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----- |
| Shareable Links | Paylaşılabilir Bağlantılar | |
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Kaynaklarınıza paylaşılabilir bağlantılar oluşturun. Bağlantılar, kaynağınıza geçici veya sınırsız erişim sağlar. Oluştururken bağlantının geçerlilik süresini ayarlayabilirsiniz. | |
| Easy to create and share | Oluşturması ve paylaşması kolay | |
| Configurable expiration duration | Yapılandırılabilir geçerlilik süresi | |
| Secure and revocable | Güvenli ve iptal edilebilir | |
##### Content
| EN | TR | Notes |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | -------------- |
| Manage Shareable Links | Paylaşılabilir Bağlantıları Yönet | |
| Create shareable links to grant temporary or permanent access to your resources | Kaynaklarınıza geçici veya kalıcı erişim sağlamak için paylaşılabilir bağlantılar oluşturun | |
| Search links | Bağlantıları ara | placeholder |
| Create Share Link | Bağlantı Oluştur | addbutton |
| Resource | Kaynak | table header |
| Title | Başlık | table header |
| Created | Oluşturulma Tarihi | table header |
| Expires | Son Kullanma Tarihi | table header |
| No links. Create one to get started. | Bağlantı yok. Başlamak için bir tane oluşturun. | table placeholder |
##### Create Shareable Link popup
| EN | TR | Notes |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| Create Shareable Link | Paylaşılabilir Bağlantı Oluştur | |
| Anyone with this link can access the resource | Bu bağlantıya sahip olan herkes kaynağa erişebilir | |
| Resource | Kaynak | |
| Select resource | Kaynak seçin | |
| Search resources… | Kaynak ara… | resource selector popup |
| Title (optional) | Başlık (isteğe bağlı) | |
| Enter title | Başlık girin | placeholder |
| Expire in | Sona Erme Süresi | |
| Minutes | Dakika | |
| Hours | Saat | |
| Days | Gün | |
| Months | Ay | |
| Years | Yıl | |
| Never expire | Asla sona erme | |
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Bağlantının geçerlilik süresi, bağlantının ne kadar süreyle kullanılabilir olacağını ve kaynağa erişim sağlayacağını belirler. Bu sürenin sonunda bağlantı çalışmaz hale gelir ve bağlantıyı kullananlar kaynağa erişimini kaybeder. | |
| Create Link | Bağlantı Oluştur | |
| Close | Kapat | |
---
## Main “General”
| EN | TR | Notes |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------ |
| General | Genel | |
| Configure your organizations general settings | Organizasyonunuzun genel ayarlarını yapılandırın | |
| General | Genel | sidebar item |
| Organization Settings | Organizasyon Ayarları | |
| Manage your organization details and configuration | Organizasyonunuzun detaylarını ve yapılandırmasını yönetin | |
| Name | Ad | |
| This is the display name of the org | Bu, organizasyonunuzun görüntülenecek adıdır. | |
| Save Settings | Ayarları Kaydet | |
| Danger Zone | Tehlikeli Bölge | |
| Once you delete this org, there is no going back. Please be certain. | Bu organizasyonu sildikten sonra geri dönüş yoktur. Lütfen emin olun. | |
| Delete Organization Data | Organizasyon Verilerini Sil | |

View File

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

16418
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
},
};

View File

@@ -1,22 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="900.82861"
height="955.20648"
viewBox="0 0 238.34422 252.7317"
version="1.1"
x="0px"
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="noun-pangolin-1798092.svg"
width="400"
height="400"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
id="svg420"
inkscape:export-filename="logo.svg"
inkscape:export-xdpi="221.14999"
inkscape:export-ydpi="221.14999"
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:svg="http://www.w3.org/2000/svg"><defs
id="defs56" /><sodipodi:namedview
id="namedview54"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview422"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
@@ -24,15 +23,18 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.9583914"
inkscape:cx="209.86611"
inkscape:cy="262.20499"
inkscape:window-width="3840"
inkscape:window-height="2136"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
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" /></svg>
inkscape:document-units="mm"
showgrid="false" />
<defs
id="defs417" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-13.119542,-5.9258171)">
<path
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"
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"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="900.82861"
height="955.20648"
viewBox="0 0 238.34422 252.7317"
version="1.1"
x="0px"
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"
id="svg420"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs56" /><sodipodi:namedview
id="namedview54"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.9583914"
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>
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs417" />
<g
id="layer1"
transform="translate(-13.119542,-5.9258171)">
<path
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"
style="fill:#f36118;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: 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

BIN
public/screenshots/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

View File

@@ -14,7 +14,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod";
const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() {

View File

@@ -1,11 +1,14 @@
import { Request } from "express";
import { db } from "@server/db";
import { userActions, roleActions, userOrgs } from "@server/db/schema";
import { userActions, roleActions, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export enum ActionsEnum {
createOrgUser = "createOrgUser",
listOrgs = "listOrgs",
listUserOrgs = "listUserOrgs",
createOrg = "createOrg",
// deleteOrg = "deleteOrg",
getOrg = "getOrg",
@@ -32,6 +35,8 @@ export enum ActionsEnum {
listRoles = "listRoles",
updateRole = "updateRole",
inviteUser = "inviteUser",
listInvitations = "listInvitations",
removeInvitation = "removeInvitation",
removeUser = "removeUser",
listUsers = "listUsers",
listSiteRoles = "listSiteRoles",
@@ -62,6 +67,25 @@ export enum ActionsEnum {
deleteResourceRule = "deleteResourceRule",
listResourceRules = "listResourceRules",
updateResourceRule = "updateResourceRule",
listOrgDomains = "listOrgDomains",
createNewt = "createNewt",
createIdp = "createIdp",
updateIdp = "updateIdp",
deleteIdp = "deleteIdp",
listIdps = "listIdps",
getIdp = "getIdp",
createIdpOrg = "createIdpOrg",
deleteIdpOrg = "deleteIdpOrg",
listIdpOrgs = "listIdpOrgs",
updateIdpOrg = "updateIdpOrg",
checkOrgId = "checkOrgId",
createApiKey = "createApiKey",
deleteApiKey = "deleteApiKey",
setApiKeyActions = "setApiKeyActions",
setApiKeyOrgs = "setApiKeyOrgs",
listApiKeyActions = "listApiKeyActions",
listApiKeys = "listApiKeys",
getApiKey = "getApiKey"
}
export async function checkUserActionPermission(

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { db } from '@server/db';
import { limitsTable } from '@server/db/schema';
import { limitsTable } from '@server/db';
import { and, eq } from 'drizzle-orm';
import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
@@ -37,4 +37,4 @@ export async function checkOrgLimit({ orgId, limitName, currentValue, increment
}
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Unknown error occurred while checking limit');
}
}
}

View File

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

View File

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

View File

@@ -9,9 +9,9 @@ import {
sessions,
User,
users
} from "@server/db/schema";
import db from "@server/db";
import { eq } from "drizzle-orm";
} from "@server/db";
import { db } from "@server/db";
import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config";
import type { RandomReader } from "@oslojs/crypto/random";
import { generateRandomString } from "@oslojs/crypto/random";
@@ -95,28 +95,53 @@ export async function validateSessionToken(
}
export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(resourceSessions).where(eq(resourceSessions.userSessionId, sessionId));
await db.delete(sessions).where(eq(sessions.sessionId, sessionId));
try {
await db.transaction(async (trx) => {
await trx
.delete(resourceSessions)
.where(eq(resourceSessions.userSessionId, sessionId));
await trx.delete(sessions).where(eq(sessions.sessionId, sessionId));
});
} catch (e) {
logger.error("Failed to invalidate session", e);
}
}
export async function invalidateAllSessions(userId: string): Promise<void> {
await db.delete(sessions).where(eq(sessions.userId, userId));
try {
await db.transaction(async (trx) => {
const userSessions = await trx
.select()
.from(sessions)
.where(eq(sessions.userId, userId));
await trx.delete(resourceSessions).where(
inArray(
resourceSessions.userSessionId,
userSessions.map((s) => s.sessionId)
)
);
await trx.delete(sessions).where(eq(sessions.userId, userId));
});
} catch (e) {
logger.error("Failed to all invalidate user sessions", e);
}
}
export function serializeSessionCookie(
token: string,
isSecure: boolean
isSecure: boolean,
expiresAt: Date
): string {
if (isSecure) {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
} else {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/;`;
}
}
export function createBlankSessionTokenCookie(isSecure: boolean): string {
if (isSecure) {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
} else {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
}

View File

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

View File

@@ -1,7 +1,7 @@
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { resourceSessions, ResourceSession } from "@server/db/schema";
import db from "@server/db";
import { resourceSessions, ResourceSession } from "@server/db";
import { db } from "@server/db";
import { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
@@ -167,12 +167,20 @@ export function serializeResourceSessionCookie(
cookieName: string,
domain: string,
token: string,
isHttp: boolean = false
isHttp: boolean = false,
expiresAt?: Date
): string {
const now = new Date().getTime();
if (!isHttp) {
return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
if (expiresAt === undefined) {
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`;
}
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`;
} else {
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
if (expiresAt === undefined) {
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`;
}
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`;
}
}
@@ -182,9 +190,9 @@ export function createBlankResourceSessionTokenCookie(
isHttp: boolean = false
): string {
if (!isHttp) {
return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
return `${cookieName}_s=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
} else {
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`;
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${"." + domain}`;
}
}

View File

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

View File

@@ -1,55 +1,97 @@
import db from "@server/db";
import { db } from "@server/db";
import {
Resource,
ResourceAccessToken,
resourceAccessToken,
} from "@server/db/schema";
resources
} from "@server/db";
import { and, eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "./password";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
export async function verifyResourceAccessToken({
resource,
accessToken,
accessTokenId,
accessToken
resourceId
}: {
resource: Resource;
accessTokenId: string;
accessToken: string;
accessTokenId?: string;
resourceId?: number; // IF THIS IS NOT SET, THE TOKEN IS VALID FOR ALL RESOURCES
}): Promise<{
valid: boolean;
error?: string;
tokenItem?: ResourceAccessToken;
resource?: Resource;
}> {
const [result] = await db
.select()
.from(resourceAccessToken)
.where(
and(
eq(resourceAccessToken.resourceId, resource.resourceId),
eq(resourceAccessToken.accessTokenId, accessTokenId)
)
)
.limit(1);
const accessTokenHash = encodeHexLowerCase(
sha256(new TextEncoder().encode(accessToken))
);
const tokenItem = result;
let tokenItem: ResourceAccessToken | undefined;
let resource: Resource | undefined;
if (!tokenItem) {
if (!accessTokenId) {
const [res] = await db
.select()
.from(resourceAccessToken)
.where(and(eq(resourceAccessToken.tokenHash, accessTokenHash)))
.innerJoin(
resources,
eq(resourceAccessToken.resourceId, resources.resourceId)
);
tokenItem = res?.resourceAccessToken;
resource = res?.resources;
} else {
const [res] = await db
.select()
.from(resourceAccessToken)
.where(and(eq(resourceAccessToken.accessTokenId, accessTokenId)))
.innerJoin(
resources,
eq(resourceAccessToken.resourceId, resources.resourceId)
);
if (res && res.resourceAccessToken) {
if (res.resourceAccessToken.tokenHash?.startsWith("$argon")) {
const validCode = await verifyPassword(
accessToken,
res.resourceAccessToken.tokenHash
);
if (!validCode) {
return {
valid: false,
error: "Invalid access token"
};
}
} else {
const tokenHash = encodeHexLowerCase(
sha256(new TextEncoder().encode(accessToken))
);
if (res.resourceAccessToken.tokenHash !== tokenHash) {
return {
valid: false,
error: "Invalid access token"
};
}
}
}
tokenItem = res?.resourceAccessToken;
resource = res?.resources;
}
if (!tokenItem || !resource) {
return {
valid: false,
error: "Access token does not exist for resource"
};
}
const validCode = await verifyPassword(accessToken, tokenItem.tokenHash);
if (!validCode) {
return {
valid: false,
error: "Invalid access token"
};
}
if (
tokenItem.expiresAt &&
!isWithinExpirationDate(new Date(tokenItem.expiresAt))
@@ -60,8 +102,16 @@ export async function verifyResourceAccessToken({
};
}
if (resourceId && resource.resourceId !== resourceId) {
return {
valid: false,
error: "Resource ID does not match"
};
}
return {
valid: true,
tokenItem
tokenItem,
resource
};
}

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";
import Database from "better-sqlite3";
import * as schema from "@server/db/schema";
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 });
}
}
export * from "./sqlite";
// export * from "./pg";

View File

@@ -1,7 +1,7 @@
import { join } from "path";
import { readFileSync } from "fs";
import { db } from "@server/db";
import { exitNodes, sites } from "./schema";
import { exitNodes, sites } from "@server/db";
import { eq, and } from "drizzle-orm";
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;

2
server/db/pg/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./driver";
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 db from "@server/db";
import db from "./driver";
import path from "path";
const migrationsFolder = path.join("server/migrations");
@@ -7,7 +7,7 @@ const migrationsFolder = path.join("server/migrations");
const runMigrations = async () => {
console.log("Running migrations...");
try {
migrate(db, {
migrate(db as any, {
migrationsFolder: migrationsFolder,
});
console.log("Migrations completed successfully.");

View File

@@ -1,10 +1,26 @@
import { InferSelectModel } from "drizzle-orm";
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(),
baseDomain: text("baseDomain").notNull(),
configManaged: integer("configManaged", { mode: "boolean" })
.notNull()
.default(false)
});
export const orgs = sqliteTable("orgs", {
orgId: text("orgId").primaryKey(),
name: text("name").notNull(),
domain: text("domain").notNull()
name: text("name").notNull()
});
export const orgDomains = sqliteTable("orgDomains", {
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" })
});
export const sites = sqliteTable("sites", {
@@ -25,7 +41,10 @@ export const sites = sqliteTable("sites", {
megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
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", {
@@ -43,6 +62,9 @@ export const resources = sqliteTable("resources", {
name: text("name").notNull(),
subdomain: text("subdomain"),
fullDomain: text("fullDomain"),
domainId: text("domainId").references(() => domains.domainId, {
onDelete: "set null"
}),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
blockAccess: integer("blockAccess", { mode: "boolean" })
.notNull()
@@ -55,7 +77,15 @@ export const resources = sqliteTable("resources", {
.notNull()
.default(false),
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
applyRules: integer("applyRules", { mode: "boolean" }).notNull().default(false)
applyRules: integer("applyRules", { mode: "boolean" })
.notNull()
.default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
stickySession: integer("stickySession", { mode: "boolean" })
.notNull()
.default(false),
tlsServerName: text("tlsServerName"),
setHostHeader: text("setHostHeader")
});
export const targets = sqliteTable("targets", {
@@ -77,15 +107,21 @@ export const exitNodes = sqliteTable("exitNodes", {
name: text("name").notNull(),
address: text("address").notNull(), // this is the address of the wireguard interface in gerbil
endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config
publicKey: text("pubicKey").notNull(),
publicKey: text("publicKey").notNull(),
listenPort: integer("listenPort").notNull(),
reachableAt: text("reachableAt") // this is the internal address of the gerbil http server for command control
});
export const users = sqliteTable("user", {
userId: text("id").primaryKey(),
email: text("email").notNull().unique(),
passwordHash: text("passwordHash").notNull(),
email: text("email"),
username: text("username").notNull(),
name: text("name"),
type: text("type").notNull(), // "internal", "oidc"
idpId: integer("idpId").references(() => idp.idpId, {
onDelete: "cascade"
}),
passwordHash: text("passwordHash"),
twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" })
.notNull()
.default(false),
@@ -384,6 +420,98 @@ export const resourceRules = sqliteTable("resourceRules", {
value: text("value").notNull()
});
export const supporterKey = sqliteTable("supporterKey", {
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
key: text("key").notNull(),
githubUsername: text("githubUsername").notNull(),
phrase: text("phrase"),
tier: text("tier"),
valid: integer("valid", { mode: "boolean" }).notNull().default(false)
});
// Identity Providers
export const idp = sqliteTable("idp", {
idpId: integer("idpId").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
type: text("type").notNull(),
defaultRoleMapping: text("defaultRoleMapping"),
defaultOrgMapping: text("defaultOrgMapping"),
autoProvision: integer("autoProvision", {
mode: "boolean"
})
.notNull()
.default(false)
});
// Identity Provider OAuth Configuration
export const idpOidcConfig = sqliteTable("idpOidcConfig", {
idpOauthConfigId: integer("idpOauthConfigId").primaryKey({
autoIncrement: true
}),
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
clientId: text("clientId").notNull(),
clientSecret: text("clientSecret").notNull(),
authUrl: text("authUrl").notNull(),
tokenUrl: text("tokenUrl").notNull(),
identifierPath: text("identifierPath").notNull(),
emailPath: text("emailPath"),
namePath: text("namePath"),
scopes: text("scopes").notNull()
});
export const licenseKey = sqliteTable("licenseKey", {
licenseKeyId: text("licenseKeyId").primaryKey().notNull(),
instanceId: text("instanceId").notNull(),
token: text("token").notNull()
});
export const hostMeta = sqliteTable("hostMeta", {
hostMetaId: text("hostMetaId").primaryKey().notNull(),
createdAt: integer("createdAt").notNull()
});
export const apiKeys = sqliteTable("apiKeys", {
apiKeyId: text("apiKeyId").primaryKey(),
name: text("name").notNull(),
apiKeyHash: text("apiKeyHash").notNull(),
lastChars: text("lastChars").notNull(),
createdAt: text("dateCreated").notNull(),
isRoot: integer("isRoot", { mode: "boolean" }).notNull().default(false)
});
export const apiKeyActions = sqliteTable("apiKeyActions", {
apiKeyId: text("apiKeyId")
.notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
actionId: text("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" })
});
export const apiKeyOrg = sqliteTable("apiKeyOrg", {
apiKeyId: text("apiKeyId")
.notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const idpOrg = sqliteTable("idpOrg", {
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
roleMapping: text("roleMapping"),
orgMapping: text("orgMapping")
});
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
@@ -417,3 +545,9 @@ 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

@@ -3,6 +3,7 @@ export * from "@server/emails/sendEmail";
import nodemailer from "nodemailer";
import config from "@server/lib/config";
import logger from "@server/logger";
import SMTPTransport from "nodemailer/lib/smtp-transport";
function createEmailClient() {
const emailConfig = config.getRawConfig().email;
@@ -13,7 +14,7 @@ function createEmailClient() {
return;
}
return nodemailer.createTransport({
const settings = {
host: emailConfig.smtp_host,
port: emailConfig.smtp_port,
secure: emailConfig.smtp_secure || false,
@@ -21,7 +22,15 @@ function createEmailClient() {
user: emailConfig.smtp_user,
pass: emailConfig.smtp_pass
}
});
} as SMTPTransport.Options;
if (emailConfig.smtp_tls_reject_unauthorized !== undefined) {
settings.tls = {
rejectUnauthorized: emailConfig.smtp_tls_reject_unauthorized
};
}
return nodemailer.createTransport(settings);
}
export const emailClient = createEmailClient();

6
server/extendZod.ts Normal file
View File

@@ -0,0 +1,6 @@
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
extendZodWithOpenApi(z);
export default function extendZod() {}

View File

@@ -1,8 +1,12 @@
import "./extendZod.ts";
import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer";
import { Session, User, UserOrg } from "./db/schema";
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer";
import config from "@server/lib/config";
async function startServers() {
await runSetupFunctions();
@@ -12,10 +16,16 @@ async function startServers() {
const internalServer = createInternalServer();
const nextServer = await createNextServer();
let integrationServer;
if (config.getRawConfig().flags?.enable_integration_api) {
integrationServer = createIntegrationApiServer();
}
return {
apiServer,
nextServer,
internalServer,
integrationServer
};
}
@@ -23,9 +33,11 @@ async function startServers() {
declare global {
namespace Express {
interface Request {
apiKey?: ApiKey;
user?: User;
session?: Session;
userOrg?: UserOrg;
apiKeyOrg?: ApiKeyOrg;
userOrgRoleId?: number;
userOrgId?: string;
userOrgIds?: string[];

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 } from "drizzle-orm";
import { roleResources, userResources } from "@server/db/schema";
import { roleResources, userResources } from "@server/db";
export async function canUserAccessResource({
userId,

View File

@@ -1,363 +1,178 @@
import fs from "fs";
import yaml from "js-yaml";
import path from "path";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import {
__DIRNAME,
APP_PATH,
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 hostnameSchema = z
.string()
.regex(
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/
)
.or(z.literal("localhost"));
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
return process.env[envVar] ?? valFromYaml;
};
const configSchema = z.object({
app: z.object({
dashboard_url: z
.string()
.url()
.optional()
.transform(getEnvOrYaml("APP_DASHBOARDURL"))
.pipe(z.string().url())
.transform((url) => url.toLowerCase()),
base_domain: hostnameSchema
.optional()
.transform(getEnvOrYaml("APP_BASEDOMAIN"))
.pipe(hostnameSchema)
.transform((url) => url.toLowerCase()),
log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean(),
log_failed_attempts: z.boolean().optional()
}),
server: z.object({
external_port: portSchema
.optional()
.transform(getEnvOrYaml("SERVER_EXTERNALPORT"))
.transform(stoi)
.pipe(portSchema),
internal_port: portSchema
.optional()
.transform(getEnvOrYaml("SERVER_INTERNALPORT"))
.transform(stoi)
.pipe(portSchema),
next_port: portSchema
.optional()
.transform(getEnvOrYaml("SERVER_NEXTPORT"))
.transform(stoi)
.pipe(portSchema),
internal_hostname: z.string().transform((url) => url.toLowerCase()),
session_cookie_name: z.string(),
resource_access_token_param: 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)
}),
traefik: z.object({
http_entrypoint: z.string(),
https_entrypoint: z.string().optional(),
cert_resolver: z.string().optional(),
prefer_wildcard_cert: z.boolean().optional(),
additional_middlewares: z.array(z.string()).optional()
}),
gerbil: z.object({
start_port: portSchema
.optional()
.transform(getEnvOrYaml("GERBIL_STARTPORT"))
.transform(stoi)
.pipe(portSchema),
base_endpoint: z
.string()
.optional()
.transform(getEnvOrYaml("GERBIL_BASEENDPOINT"))
.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(),
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()
})
.optional()
});
import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
import { db } from "@server/db";
import { SupporterKey, supporterKey } from "@server/db";
import { eq } from "drizzle-orm";
import { license } from "@server/license/license";
import { configSchema, readConfigFile } from "./readConfigFile";
export class Config {
private rawConfig!: z.infer<typeof configSchema>;
constructor() {
this.loadConfig();
supporterData: SupporterKey | null = null;
if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
this.createTraefikConfig();
}
supporterHiddenUntil: number | null = null;
isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() {
this.load();
}
public loadEnvironment() {}
public loadConfig() {
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 (!environment) {
const exampleConfigPath = path.join(
__DIRNAME,
"config.example.yml"
);
if (fs.existsSync(exampleConfigPath)) {
try {
const exampleConfigContent = fs.readFileSync(
exampleConfigPath,
"utf8"
);
fs.writeFileSync(
configFilePath1,
exampleConfigContent,
"utf8"
);
environment = loadConfig(configFilePath1);
} catch (error) {
console.log(
"See the docs for information about what to include in the configuration file: https://docs.fossorial.io/Pangolin/Configuration/config"
);
if (error instanceof Error) {
throw new Error(
`Error creating configuration file from example: ${
error.message
}`
);
}
throw error;
}
} else {
throw new Error(
"No configuration file found and no example configuration available"
);
}
}
if (!environment) {
throw new Error("No configuration file found");
}
const parsedConfig = configSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid configuration file: ${errors}`);
}
public load() {
const parsedConfig = readConfigFile();
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 =
parsedConfig.data.server.external_port.toString();
parsedConfig.server.external_port.toString();
process.env.SERVER_INTERNAL_PORT =
parsedConfig.data.server.internal_port.toString();
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
parsedConfig.server.internal_port.toString();
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.flags
?.require_email_verification
? "true"
: "false";
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.flags
?.allow_raw_resources
? "true"
: "false";
process.env.SESSION_COOKIE_NAME =
parsedConfig.data.server.session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
parsedConfig.server.session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.email ? "true" : "false";
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.flags
?.disable_signup_without_invite
? "true"
: "false";
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.flags
?.disable_user_create_org
? "true"
: "false";
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 =
parsedConfig.server.resource_access_token_headers.id;
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN =
parsedConfig.server.resource_access_token_headers.token;
process.env.RESOURCE_SESSION_REQUEST_PARAM =
parsedConfig.data.server.resource_session_request_param;
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
parsedConfig.server.resource_session_request_param;
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.flags
?.allow_base_domain_resources
? "true"
: "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
this.rawConfig = parsedConfig.data;
license.setServerSecret(parsedConfig.server.secret);
this.checkKeyStatus();
this.rawConfig = parsedConfig;
}
private async checkKeyStatus() {
const licenseStatus = await license.check();
if (!licenseStatus.isHostLicensed) {
this.checkSupporterKey();
}
}
public getRawConfig() {
return this.rawConfig;
}
public getBaseDomain(): string {
return this.rawConfig.app.base_domain;
}
public getNoReplyEmail(): string | undefined {
return (
this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
);
}
private createTraefikConfig() {
public getDomain(domainId: string) {
return this.rawConfig.domains[domainId];
}
public hideSupporterKey(days: number = 7) {
const now = new Date().getTime();
if (this.supporterHiddenUntil && now < this.supporterHiddenUntil) {
return;
}
this.supporterHiddenUntil = now + 1000 * 60 * 60 * 24 * days;
}
public isSupporterKeyHidden() {
const now = new Date().getTime();
if (this.supporterHiddenUntil && now < this.supporterHiddenUntil) {
return true;
}
return false;
}
public async checkSupporterKey() {
const [key] = await db.select().from(supporterKey).limit(1);
if (!key) {
return;
}
const { key: licenseKey, githubUsername } = key;
try {
// check if traefik_config.yml and dynamic_config.yml exists in APP_PATH/traefik
const defaultTraefikConfigPath = path.join(
__DIRNAME,
"traefik_config.example.yml"
);
const defaultDynamicConfigPath = path.join(
__DIRNAME,
"dynamic_config.example.yml"
const response = await fetch(
"https://api.fossorial.io/api/v1/license/validate",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKey,
githubUsername
})
}
);
const traefikPath = path.join(APP_PATH, "traefik");
if (!fs.existsSync(traefikPath)) {
if (!response.ok) {
this.supporterData = key;
return;
}
// load default configs
let traefikConfig = fs.readFileSync(
defaultTraefikConfigPath,
"utf8"
);
let dynamicConfig = fs.readFileSync(
defaultDynamicConfigPath,
"utf8"
);
const data = await response.json();
traefikConfig = traefikConfig
.split("{{.LetsEncryptEmail}}")
.join(this.rawConfig.users.server_admin.email);
traefikConfig = traefikConfig
.split("{{.INTERNAL_PORT}}")
.join(this.rawConfig.server.internal_port.toString());
if (!data.data.valid) {
this.supporterData = {
...key,
valid: false
};
return;
}
dynamicConfig = dynamicConfig
.split("{{.DashboardDomain}}")
.join(new URL(this.rawConfig.app.dashboard_url).hostname);
dynamicConfig = dynamicConfig
.split("{{.NEXT_PORT}}")
.join(this.rawConfig.server.next_port.toString());
dynamicConfig = dynamicConfig
.split("{{.EXTERNAL_PORT}}")
.join(this.rawConfig.server.external_port.toString());
this.supporterData = {
...key,
tier: data.data.tier,
valid: true
};
// write thiese to the traefik directory
const traefikConfigPath = path.join(
traefikPath,
"traefik_config.yml"
);
const dynamicConfigPath = path.join(
traefikPath,
"dynamic_config.yml"
);
fs.writeFileSync(traefikConfigPath, traefikConfig, "utf8");
fs.writeFileSync(dynamicConfigPath, dynamicConfig, "utf8");
console.log("Traefik configuration files created");
// update the supporter key in the database
await db
.update(supporterKey)
.set({
tier: data.data.tier || null,
phrase: data.data.cutePhrase || null,
valid: true
})
.where(eq(supporterKey.keyId, key.keyId));
} catch (e) {
console.log(
"Failed to generate the Traefik configuration files. Please create them manually."
);
console.error(e);
this.supporterData = key;
console.error("Failed to validate supporter key", e);
}
}
public getSupporterData() {
return this.supporterData;
}
}
export const config = new Config();

View File

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

12
server/lib/crypto.ts Normal file
View File

@@ -0,0 +1,12 @@
import CryptoJS from "crypto-js";
export function encrypt(value: string, key: string): string {
const ciphertext = CryptoJS.AES.encrypt(value, key).toString();
return ciphertext;
}
export function decrypt(encryptedValue: string, key: string): string {
const bytes = CryptoJS.AES.decrypt(encryptedValue, key);
const originalText = bytes.toString(CryptoJS.enc.Utf8);
return originalText;
}

View File

@@ -0,0 +1,8 @@
import config from "@server/lib/config";
export function generateOidcRedirectUrl(idpId: number) {
const dashboardUrl = config.getRawConfig().app.dashboard_url;
const redirectPath = `/auth/idp/${idpId}/oidc/callback`;
const redirectUrl = new URL(redirectPath, dashboardUrl).toString();
return redirectUrl;
}

127
server/lib/ip.test.ts Normal file
View File

@@ -0,0 +1,127 @@
import { cidrToRange, findNextAvailableCidr } from "./ip";
import { assertEquals } from "@test/assert";
// Test cases
function testFindNextAvailableCidr() {
console.log("Running findNextAvailableCidr tests...");
// Test 1: Basic IPv4 allocation
{
const existing = ["10.0.0.0/16", "10.1.0.0/16"];
const result = findNextAvailableCidr(existing, 16, "10.0.0.0/8");
assertEquals(result, "10.2.0.0/16", "Basic IPv4 allocation failed");
}
// Test 2: Finding gap between allocations
{
const existing = ["10.0.0.0/16", "10.2.0.0/16"];
const result = findNextAvailableCidr(existing, 16, "10.0.0.0/8");
assertEquals(result, "10.1.0.0/16", "Finding gap between allocations failed");
}
// Test 3: No available space
{
const existing = ["10.0.0.0/8"];
const result = findNextAvailableCidr(existing, 8, "10.0.0.0/8");
assertEquals(result, null, "No available space test failed");
}
// // Test 4: IPv6 allocation
// {
// const existing = ["2001:db8::/32", "2001:db8:1::/32"];
// const result = findNextAvailableCidr(existing, 32, "2001:db8::/16");
// assertEquals(result, "2001:db8:2::/32", "Basic IPv6 allocation failed");
// }
// // Test 5: Mixed IP versions
// {
// const existing = ["10.0.0.0/16", "2001:db8::/32"];
// assertThrows(
// () => findNextAvailableCidr(existing, 16),
// "All CIDRs must be of the same IP version",
// "Mixed IP versions test failed"
// );
// }
// Test 6: Empty input
{
const existing: string[] = [];
const result = findNextAvailableCidr(existing, 16);
assertEquals(result, null, "Empty input test failed");
}
// Test 7: Block size alignment
{
const existing = ["10.0.0.0/24"];
const result = findNextAvailableCidr(existing, 24, "10.0.0.0/16");
assertEquals(result, "10.0.1.0/24", "Block size alignment test failed");
}
// Test 8: Block size alignment
{
const existing: string[] = [];
const result = findNextAvailableCidr(existing, 24, "10.0.0.0/16");
assertEquals(result, "10.0.0.0/24", "Block size alignment test failed");
}
// Test 9: Large block size request
{
const existing = ["10.0.0.0/24", "10.0.1.0/24"];
const result = findNextAvailableCidr(existing, 16, "10.0.0.0/16");
assertEquals(result, null, "Large block size request test failed");
}
console.log("All findNextAvailableCidr tests passed!");
}
// function testCidrToRange() {
// console.log("Running cidrToRange tests...");
// // Test 1: Basic IPv4 conversion
// {
// const result = cidrToRange("192.168.0.0/24");
// assertEqualsObj(result, {
// start: BigInt("3232235520"),
// end: BigInt("3232235775")
// }, "Basic IPv4 conversion failed");
// }
// // Test 2: IPv6 conversion
// {
// const result = cidrToRange("2001:db8::/32");
// assertEqualsObj(result, {
// start: BigInt("42540766411282592856903984951653826560"),
// end: BigInt("42540766411282592875350729025363378175")
// }, "IPv6 conversion failed");
// }
// // Test 3: Invalid prefix length
// {
// assertThrows(
// () => cidrToRange("192.168.0.0/33"),
// "Invalid prefix length for IPv4",
// "Invalid IPv4 prefix test failed"
// );
// }
// // Test 4: Invalid IPv6 prefix
// {
// assertThrows(
// () => cidrToRange("2001:db8::/129"),
// "Invalid prefix length for IPv6",
// "Invalid IPv6 prefix test failed"
// );
// }
// console.log("All cidrToRange tests passed!");
// }
// Run all tests
try {
// testCidrToRange();
testFindNextAvailableCidr();
console.log("All tests passed successfully!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}

View File

@@ -3,58 +3,162 @@ interface IPRange {
end: bigint;
}
type IPVersion = 4 | 6;
/**
* Converts IP address string to BigInt for numerical operations
* Detects IP version from address string
*/
function detectIpVersion(ip: string): IPVersion {
return ip.includes(':') ? 6 : 4;
}
/**
* Converts IPv4 or IPv6 address string to BigInt for numerical operations
*/
function ipToBigInt(ip: string): bigint {
return ip.split('.')
.reduce((acc, octet) => BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(parseInt(octet))), BigInt(0));
const version = detectIpVersion(ip);
if (version === 4) {
return ip.split('.')
.reduce((acc, octet) => {
const num = parseInt(octet);
if (isNaN(num) || num < 0 || num > 255) {
throw new Error(`Invalid IPv4 octet: ${octet}`);
}
return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num));
}, BigInt(0));
} else {
// Handle IPv6
// Expand :: notation
let fullAddress = ip;
if (ip.includes('::')) {
const parts = ip.split('::');
if (parts.length > 2) throw new Error('Invalid IPv6 address: multiple :: found');
const missing = 8 - (parts[0].split(':').length + parts[1].split(':').length);
const padding = Array(missing).fill('0').join(':');
fullAddress = `${parts[0]}:${padding}:${parts[1]}`;
}
return fullAddress.split(':')
.reduce((acc, hextet) => {
const num = parseInt(hextet || '0', 16);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid IPv6 hextet: ${hextet}`);
}
return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num));
}, BigInt(0));
}
}
/**
* Converts BigInt to IP address string
*/
function bigIntToIp(num: bigint): string {
const octets: number[] = [];
for (let i = 0; i < 4; i++) {
octets.unshift(Number(num & BigInt(255)));
num = num >> BigInt(8);
function bigIntToIp(num: bigint, version: IPVersion): string {
if (version === 4) {
const octets: number[] = [];
for (let i = 0; i < 4; i++) {
octets.unshift(Number(num & BigInt(255)));
num = num >> BigInt(8);
}
return octets.join('.');
} else {
const hextets: string[] = [];
for (let i = 0; i < 8; i++) {
hextets.unshift(Number(num & BigInt(65535)).toString(16).padStart(4, '0'));
num = num >> BigInt(16);
}
// Compress zero sequences
let maxZeroStart = -1;
let maxZeroLength = 0;
let currentZeroStart = -1;
let currentZeroLength = 0;
for (let i = 0; i < hextets.length; i++) {
if (hextets[i] === '0000') {
if (currentZeroStart === -1) currentZeroStart = i;
currentZeroLength++;
if (currentZeroLength > maxZeroLength) {
maxZeroLength = currentZeroLength;
maxZeroStart = currentZeroStart;
}
} else {
currentZeroStart = -1;
currentZeroLength = 0;
}
}
if (maxZeroLength > 1) {
hextets.splice(maxZeroStart, maxZeroLength, '');
if (maxZeroStart === 0) hextets.unshift('');
if (maxZeroStart + maxZeroLength === 8) hextets.push('');
}
return hextets.map(h => h === '0000' ? '0' : h.replace(/^0+/, '')).join(':');
}
return octets.join('.');
}
/**
* Converts CIDR to IP range
*/
function cidrToRange(cidr: string): IPRange {
export function cidrToRange(cidr: string): IPRange {
const [ip, prefix] = cidr.split('/');
const version = detectIpVersion(ip);
const prefixBits = parseInt(prefix);
const ipBigInt = ipToBigInt(ip);
const mask = BigInt.asUintN(64, (BigInt(1) << BigInt(32 - prefixBits)) - BigInt(1));
// Validate prefix length
const maxPrefix = version === 4 ? 32 : 128;
if (prefixBits < 0 || prefixBits > maxPrefix) {
throw new Error(`Invalid prefix length for IPv${version}: ${prefix}`);
}
const shiftBits = BigInt(maxPrefix - prefixBits);
const mask = BigInt.asUintN(version === 4 ? 64 : 128, (BigInt(1) << shiftBits) - BigInt(1));
const start = ipBigInt & ~mask;
const end = start | mask;
return { start, end };
}
/**
* Finds the next available CIDR block given existing allocations
* @param existingCidrs Array of existing CIDR blocks
* @param blockSize Desired prefix length for the new block (e.g., 24 for /24)
* @param startCidr Optional CIDR to start searching from (default: "0.0.0.0/0")
* @param blockSize Desired prefix length for the new block
* @param startCidr Optional CIDR to start searching from
* @returns Next available CIDR block or null if none found
*/
export function findNextAvailableCidr(
existingCidrs: string[],
blockSize: number,
startCidr: string = "0.0.0.0/0"
startCidr?: string
): string | null {
if (!startCidr && existingCidrs.length === 0) {
return null;
}
// If no existing CIDRs, use the IP version from startCidr
const version = startCidr
? detectIpVersion(startCidr.split('/')[0])
: 4; // Default to IPv4 if no startCidr provided
// Use appropriate default startCidr if none provided
startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");
// If there are existing CIDRs, ensure all are same version
if (existingCidrs.length > 0 &&
existingCidrs.some(cidr => detectIpVersion(cidr.split('/')[0]) !== version)) {
throw new Error('All CIDRs must be of the same IP version');
}
// Convert existing CIDRs to ranges and sort them
const existingRanges = existingCidrs
.map(cidr => cidrToRange(cidr))
.sort((a, b) => (a.start < b.start ? -1 : 1));
// Calculate block size
const blockSizeBigInt = BigInt(1) << BigInt(32 - blockSize);
const maxPrefix = version === 4 ? 32 : 128;
const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize);
// Start from the beginning of the given CIDR
let current = cidrToRange(startCidr).start;
@@ -63,7 +167,6 @@ export function findNextAvailableCidr(
// Iterate through existing ranges
for (let i = 0; i <= existingRanges.length; i++) {
const nextRange = existingRanges[i];
// Align current to block size
const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);
@@ -74,7 +177,7 @@ export function findNextAvailableCidr(
// If we're at the end of existing ranges or found a gap
if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) {
return `${bigIntToIp(alignedCurrent)}/${blockSize}`;
return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`;
}
// Move current pointer to after the current range
@@ -85,13 +188,22 @@ export function findNextAvailableCidr(
}
/**
* Checks if a given IP address is within a CIDR range
* @param ip IP address to check
* @param cidr CIDR range to check against
* @returns boolean indicating if IP is within the CIDR range
*/
* Checks if a given IP address is within a CIDR range
* @param ip IP address to check
* @param cidr CIDR range to check against
* @returns boolean indicating if IP is within the CIDR range
*/
export function isIpInCidr(ip: string, cidr: string): boolean {
const ipVersion = detectIpVersion(ip);
const cidrVersion = detectIpVersion(cidr.split('/')[0]);
// If IP versions don't match, the IP cannot be in the CIDR range
if (ipVersion !== cidrVersion) {
// throw new Erorr
return false;
}
const ipBigInt = ipToBigInt(ip);
const range = cidrToRange(cidr);
return ipBigInt >= range.start && ipBigInt <= range.end;
}
}

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,3 +9,10 @@ export const subdomainSchema = z
.min(1, "Subdomain must be at least 1 character long")
.transform((val) => val.toLowerCase());
export const tlsNameSchema = z
.string()
.regex(
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$|^$/,
"Invalid subdomain format"
)
.transform((val) => val.toLowerCase());

View File

@@ -0,0 +1,71 @@
import { isValidUrlGlobPattern } from "./validators";
import { assertEquals } from "@test/assert";
function runTests() {
console.log('Running URL pattern validation tests...');
// Test valid patterns
assertEquals(isValidUrlGlobPattern('simple'), true, 'Simple path segment should be valid');
assertEquals(isValidUrlGlobPattern('simple/path'), true, 'Simple path with slash should be valid');
assertEquals(isValidUrlGlobPattern('/leading/slash'), true, 'Path with leading slash should be valid');
assertEquals(isValidUrlGlobPattern('path/'), true, 'Path with trailing slash should be valid');
assertEquals(isValidUrlGlobPattern('path/*'), true, 'Path with wildcard segment should be valid');
assertEquals(isValidUrlGlobPattern('*'), true, 'Single wildcard should be valid');
assertEquals(isValidUrlGlobPattern('*/subpath'), true, 'Wildcard with subpath should be valid');
assertEquals(isValidUrlGlobPattern('path/*/more'), true, 'Path with wildcard in the middle should be valid');
// Test with special characters
assertEquals(isValidUrlGlobPattern('path-with-dash'), true, 'Path with dash should be valid');
assertEquals(isValidUrlGlobPattern('path_with_underscore'), true, 'Path with underscore should be valid');
assertEquals(isValidUrlGlobPattern('path.with.dots'), true, 'Path with dots should be valid');
assertEquals(isValidUrlGlobPattern('path~with~tilde'), true, 'Path with tilde should be valid');
assertEquals(isValidUrlGlobPattern('path!with!exclamation'), true, 'Path with exclamation should be valid');
assertEquals(isValidUrlGlobPattern('path$with$dollar'), true, 'Path with dollar should be valid');
assertEquals(isValidUrlGlobPattern('path&with&ampersand'), true, 'Path with ampersand should be valid');
assertEquals(isValidUrlGlobPattern("path'with'quote"), true, "Path with quote should be valid");
assertEquals(isValidUrlGlobPattern('path(with)parentheses'), true, 'Path with parentheses should be valid');
assertEquals(isValidUrlGlobPattern('path+with+plus'), true, 'Path with plus should be valid');
assertEquals(isValidUrlGlobPattern('path,with,comma'), true, 'Path with comma should be valid');
assertEquals(isValidUrlGlobPattern('path;with;semicolon'), true, 'Path with semicolon should be valid');
assertEquals(isValidUrlGlobPattern('path=with=equals'), true, 'Path with equals should be valid');
assertEquals(isValidUrlGlobPattern('path:with:colon'), true, 'Path with colon should be valid');
assertEquals(isValidUrlGlobPattern('path@with@at'), true, 'Path with at should be valid');
// Test with percent encoding
assertEquals(isValidUrlGlobPattern('path%20with%20spaces'), true, 'Path with percent-encoded spaces should be valid');
assertEquals(isValidUrlGlobPattern('path%2Fwith%2Fencoded%2Fslashes'), true, 'Path with percent-encoded slashes should be valid');
// Test with wildcards in segments (the fixed functionality)
assertEquals(isValidUrlGlobPattern('padbootstrap*'), true, 'Path with wildcard at the end of segment should be valid');
assertEquals(isValidUrlGlobPattern('pad*bootstrap'), true, 'Path with wildcard in the middle of segment should be valid');
assertEquals(isValidUrlGlobPattern('*bootstrap'), true, 'Path with wildcard at the start of segment should be valid');
assertEquals(isValidUrlGlobPattern('multiple*wildcards*in*segment'), true, 'Path with multiple wildcards in segment should be valid');
assertEquals(isValidUrlGlobPattern('wild*/cards/in*/different/seg*ments'), true, 'Path with wildcards in different segments should be valid');
// Test invalid patterns
assertEquals(isValidUrlGlobPattern(''), false, 'Empty string should be invalid');
assertEquals(isValidUrlGlobPattern('//double/slash'), false, 'Path with double slash should be invalid');
assertEquals(isValidUrlGlobPattern('path//end'), false, 'Path with double slash in the middle should be invalid');
assertEquals(isValidUrlGlobPattern('invalid<char>'), false, 'Path with invalid characters should be invalid');
assertEquals(isValidUrlGlobPattern('invalid|char'), false, 'Path with invalid pipe character should be invalid');
assertEquals(isValidUrlGlobPattern('invalid"char'), false, 'Path with invalid quote character should be invalid');
assertEquals(isValidUrlGlobPattern('invalid`char'), false, 'Path with invalid backtick character should be invalid');
assertEquals(isValidUrlGlobPattern('invalid^char'), false, 'Path with invalid caret character should be invalid');
assertEquals(isValidUrlGlobPattern('invalid\\char'), false, 'Path with invalid backslash character should be invalid');
assertEquals(isValidUrlGlobPattern('invalid[char]'), false, 'Path with invalid square brackets should be invalid');
assertEquals(isValidUrlGlobPattern('invalid{char}'), false, 'Path with invalid curly braces should be invalid');
// Test invalid percent encoding
assertEquals(isValidUrlGlobPattern('invalid%2'), false, 'Path with incomplete percent encoding should be invalid');
assertEquals(isValidUrlGlobPattern('invalid%GZ'), false, 'Path with invalid hex in percent encoding should be invalid');
assertEquals(isValidUrlGlobPattern('invalid%'), false, 'Path with isolated percent sign should be invalid');
console.log('All tests passed!');
}
// Run all tests
try {
runTests();
} catch (error) {
console.error('Test failed:', error);
}

View File

@@ -9,6 +9,10 @@ export function isValidIP(ip: string): boolean {
}
export function isValidUrlGlobPattern(pattern: string): boolean {
if (pattern === "/") {
return true;
}
// Remove leading slash if present
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
@@ -24,21 +28,68 @@ export function isValidUrlGlobPattern(pattern: string): boolean {
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
// Empty segments are not allowed (double slashes)
// Empty segments are not allowed (double slashes), except at the end
if (!segment && i !== segments.length - 1) {
return false;
}
// If segment contains *, it must be exactly *
if (segment.includes("*") && segment !== "*") {
return false;
}
// Check each character in the segment
for (let j = 0; j < segment.length; j++) {
const char = segment[j];
// Check for invalid characters
if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
return false;
// Check for percent-encoded sequences
if (char === "%" && j + 2 < segment.length) {
const hex1 = segment[j + 1];
const hex2 = segment[j + 2];
if (
!/^[0-9A-Fa-f]$/.test(hex1) ||
!/^[0-9A-Fa-f]$/.test(hex2)
) {
return false;
}
j += 2; // Skip the next two characters
continue;
}
// Allow:
// - unreserved (A-Z a-z 0-9 - . _ ~)
// - sub-delims (! $ & ' ( ) * + , ; =)
// - @ : for compatibility with some systems
if (!/^[A-Za-z0-9\-._~!$&'()*+,;#=@:]$/.test(char)) {
return false;
}
}
}
return true;
}
export function isUrlValid(url: string | undefined) {
if (!url) return true; // the link is optional in the schema so if it's empty it's valid
var pattern = new RegExp(
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i"
);
return !!pattern.test(url);
}
export function isTargetValid(value: string | undefined) {
if (!value) return true;
const DOMAIN_REGEX =
/^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/;
const IPV4_REGEX =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
return true;
}
return DOMAIN_REGEX.test(value);
}

488
server/license/license.ts Normal file
View File

@@ -0,0 +1,488 @@
import { db } from "@server/db";
import { hostMeta, licenseKey, sites } from "@server/db";
import logger from "@server/logger";
import NodeCache from "node-cache";
import { validateJWT } from "./licenseJwt";
import { count, eq } from "drizzle-orm";
import moment from "moment";
import { setHostMeta } from "@server/setup/setHostMeta";
import { encrypt, decrypt } from "@server/lib/crypto";
const keyTypes = ["HOST", "SITES"] as const;
type KeyType = (typeof keyTypes)[number];
const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const;
type KeyTier = (typeof keyTiers)[number];
export type LicenseStatus = {
isHostLicensed: boolean; // Are there any license keys?
isLicenseValid: boolean; // Is the license key valid?
hostId: string; // Host ID
maxSites?: number;
usedSites?: number;
tier?: KeyTier;
};
export type LicenseKeyCache = {
licenseKey: string;
licenseKeyEncrypted: string;
valid: boolean;
iat?: Date;
type?: KeyType;
tier?: KeyTier;
numSites?: number;
};
type ActivateLicenseKeyAPIResponse = {
data: {
instanceId: string;
};
success: boolean;
error: string;
message: string;
status: number;
};
type ValidateLicenseAPIResponse = {
data: {
licenseKeys: {
[key: string]: string;
};
};
success: boolean;
error: string;
message: string;
status: number;
};
type TokenPayload = {
valid: boolean;
type: KeyType;
tier: KeyTier;
quantity: number;
terminateAt: string; // ISO
iat: number; // Issued at
};
export class License {
private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds
private validationServerUrl =
"https://api.fossorial.io/api/v1/license/professional/validate";
private activationServerUrl =
"https://api.fossorial.io/api/v1/license/professional/activate";
private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval });
private licenseKeyCache = new NodeCache();
private ephemeralKey!: string;
private statusKey = "status";
private serverSecret!: string;
private publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF
FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf
CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl
apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt
h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y
zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y
LQIDAQAB
-----END PUBLIC KEY-----`;
constructor(private hostId: string) {
this.ephemeralKey = Buffer.from(
JSON.stringify({ ts: new Date().toISOString() })
).toString("base64");
setInterval(
async () => {
await this.check();
},
1000 * 60 * 60
); // 1 hour = 60 * 60 = 3600 seconds
}
public listKeys(): LicenseKeyCache[] {
const keys = this.licenseKeyCache.keys();
return keys.map((key) => {
return this.licenseKeyCache.get<LicenseKeyCache>(key)!;
});
}
public setServerSecret(secret: string) {
this.serverSecret = secret;
}
public async forceRecheck() {
this.statusCache.flushAll();
this.licenseKeyCache.flushAll();
return await this.check();
}
public async isUnlocked(): Promise<boolean> {
const status = await this.check();
if (status.isHostLicensed) {
if (status.isLicenseValid) {
return true;
}
}
return false;
}
public async check(): Promise<LicenseStatus> {
// Set used sites
const [siteCount] = await db
.select({
value: count()
})
.from(sites);
const status: LicenseStatus = {
hostId: this.hostId,
isHostLicensed: true,
isLicenseValid: false,
maxSites: undefined,
usedSites: siteCount.value
};
try {
if (this.statusCache.has(this.statusKey)) {
const res = this.statusCache.get("status") as LicenseStatus;
res.usedSites = status.usedSites;
return res;
}
// Invalidate all
this.licenseKeyCache.flushAll();
const allKeysRes = await db.select().from(licenseKey);
if (allKeysRes.length === 0) {
status.isHostLicensed = false;
return status;
}
let foundHostKey = false;
// Validate stored license keys
for (const key of allKeysRes) {
try {
// Decrypt the license key and token
const decryptedKey = decrypt(
key.licenseKeyId,
this.serverSecret
);
const decryptedToken = decrypt(
key.token,
this.serverSecret
);
const payload = validateJWT<TokenPayload>(
decryptedToken,
this.publicKey
);
this.licenseKeyCache.set<LicenseKeyCache>(decryptedKey, {
licenseKey: decryptedKey,
licenseKeyEncrypted: key.licenseKeyId,
valid: payload.valid,
type: payload.type,
tier: payload.tier,
numSites: payload.quantity,
iat: new Date(payload.iat * 1000)
});
if (payload.type === "HOST") {
foundHostKey = true;
}
} catch (e) {
logger.error(
`Error validating license key: ${key.licenseKeyId}`
);
logger.error(e);
this.licenseKeyCache.set<LicenseKeyCache>(
key.licenseKeyId,
{
licenseKey: key.licenseKeyId,
licenseKeyEncrypted: key.licenseKeyId,
valid: false
}
);
}
}
if (!foundHostKey && allKeysRes.length) {
logger.debug("No host license key found");
status.isHostLicensed = false;
}
const keys = allKeysRes.map((key) => ({
licenseKey: decrypt(key.licenseKeyId, this.serverSecret),
instanceId: decrypt(key.instanceId, this.serverSecret)
}));
let apiResponse: ValidateLicenseAPIResponse | undefined;
try {
// Phone home to validate license keys
apiResponse = await this.phoneHome(keys);
if (!apiResponse?.success) {
throw new Error(apiResponse?.error);
}
} catch (e) {
logger.error("Error communicating with license server:");
logger.error(e);
}
logger.debug("Validate response", apiResponse);
// Check and update all license keys with server response
for (const key of keys) {
try {
const cached = this.licenseKeyCache.get<LicenseKeyCache>(
key.licenseKey
)!;
const licenseKeyRes =
apiResponse?.data?.licenseKeys[key.licenseKey];
if (!apiResponse || !licenseKeyRes) {
logger.debug(
`No response from server for license key: ${key.licenseKey}`
);
if (cached.iat) {
const exp = moment(cached.iat)
.add(7, "days")
.toDate();
if (exp > new Date()) {
logger.debug(
`Using cached license key: ${key.licenseKey}, valid ${cached.valid}`
);
continue;
}
}
logger.debug(
`Can't trust license key: ${key.licenseKey}`
);
cached.valid = false;
this.licenseKeyCache.set<LicenseKeyCache>(
key.licenseKey,
cached
);
continue;
}
const payload = validateJWT<TokenPayload>(
licenseKeyRes,
this.publicKey
);
cached.valid = payload.valid;
cached.type = payload.type;
cached.tier = payload.tier;
cached.numSites = payload.quantity;
cached.iat = new Date(payload.iat * 1000);
// Encrypt the updated token before storing
const encryptedKey = encrypt(
key.licenseKey,
this.serverSecret
);
const encryptedToken = encrypt(
licenseKeyRes,
this.serverSecret
);
await db
.update(licenseKey)
.set({
token: encryptedToken
})
.where(eq(licenseKey.licenseKeyId, encryptedKey));
this.licenseKeyCache.set<LicenseKeyCache>(
key.licenseKey,
cached
);
} catch (e) {
logger.error(`Error validating license key: ${key}`);
logger.error(e);
}
}
// Compute host status
for (const key of keys) {
const cached = this.licenseKeyCache.get<LicenseKeyCache>(
key.licenseKey
)!;
logger.debug("Checking key", cached);
if (cached.type === "HOST") {
status.isLicenseValid = cached.valid;
status.tier = cached.tier;
}
if (!cached.valid) {
continue;
}
if (!status.maxSites) {
status.maxSites = 0;
}
status.maxSites += cached.numSites || 0;
}
} catch (error) {
logger.error("Error checking license status:");
logger.error(error);
}
this.statusCache.set(this.statusKey, status);
return status;
}
public async activateLicenseKey(key: string) {
// Encrypt the license key before storing
const encryptedKey = encrypt(key, this.serverSecret);
const [existingKey] = await db
.select()
.from(licenseKey)
.where(eq(licenseKey.licenseKeyId, encryptedKey))
.limit(1);
if (existingKey) {
throw new Error("License key already exists");
}
let instanceId: string | undefined;
try {
// Call activate
const apiResponse = await fetch(this.activationServerUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKey: key,
instanceName: this.hostId
})
});
const data = await apiResponse.json();
if (!data.success) {
throw new Error(`${data.message || data.error}`);
}
const response = data as ActivateLicenseKeyAPIResponse;
if (!response.data) {
throw new Error("No response from server");
}
if (!response.data.instanceId) {
throw new Error("No instance ID in response");
}
instanceId = response.data.instanceId;
} catch (error) {
throw Error(`Error activating license key: ${error}`);
}
// Phone home to validate license key
const keys = [
{
licenseKey: key,
instanceId: instanceId!
}
];
let validateResponse: ValidateLicenseAPIResponse;
try {
validateResponse = await this.phoneHome(keys);
if (!validateResponse) {
throw new Error("No response from server");
}
if (!validateResponse.success) {
throw new Error(validateResponse.error);
}
// Validate the license key
const licenseKeyRes = validateResponse.data.licenseKeys[key];
if (!licenseKeyRes) {
throw new Error("Invalid license key");
}
const payload = validateJWT<TokenPayload>(
licenseKeyRes,
this.publicKey
);
if (!payload.valid) {
throw new Error("Invalid license key");
}
const encryptedToken = encrypt(licenseKeyRes, this.serverSecret);
// Encrypt the instanceId before storing
const encryptedInstanceId = encrypt(instanceId!, this.serverSecret);
// Store the license key in the database
await db.insert(licenseKey).values({
licenseKeyId: encryptedKey,
token: encryptedToken,
instanceId: encryptedInstanceId
});
} catch (error) {
throw Error(`Error validating license key: ${error}`);
}
// Invalidate the cache and re-compute the status
return await this.forceRecheck();
}
private async phoneHome(
keys: {
licenseKey: string;
instanceId: string;
}[]
): Promise<ValidateLicenseAPIResponse> {
// Decrypt the instanceIds before sending to the server
const decryptedKeys = keys.map((key) => ({
licenseKey: key.licenseKey,
instanceId: key.instanceId
? decrypt(key.instanceId, this.serverSecret)
: key.instanceId
}));
const response = await fetch(this.validationServerUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKeys: decryptedKeys,
ephemeralKey: this.ephemeralKey,
instanceName: this.hostId
})
});
const data = await response.json();
return data as ValidateLicenseAPIResponse;
}
}
await setHostMeta();
const [info] = await db.select().from(hostMeta).limit(1);
if (!info) {
throw new Error("Host information not found");
}
export const license = new License(info.hostMetaId);
export default license;

View File

@@ -0,0 +1,109 @@
import * as crypto from "crypto";
/**
* Validates a JWT using a public key
* @param token - The JWT to validate
* @param publicKey - The public key used for verification (PEM format)
* @returns The decoded payload if validation succeeds, throws an error otherwise
*/
function validateJWT<Payload>(
token: string,
publicKey: string
): Payload {
// Split the JWT into its three parts
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}
const [encodedHeader, encodedPayload, signature] = parts;
// Decode the header to get the algorithm
const header = JSON.parse(Buffer.from(encodedHeader, "base64").toString());
const algorithm = header.alg;
// Verify the signature
const signatureInput = `${encodedHeader}.${encodedPayload}`;
const isValid = verify(signatureInput, signature, publicKey, algorithm);
if (!isValid) {
throw new Error("Invalid signature");
}
// Decode the payload
const payload = JSON.parse(
Buffer.from(encodedPayload, "base64").toString()
);
// Check if the token has expired
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) {
throw new Error("Token has expired");
}
return payload;
}
/**
* Verifies the signature of a JWT
*/
function verify(
input: string,
signature: string,
publicKey: string,
algorithm: string
): boolean {
let verifyAlgorithm: string;
// Map JWT algorithm name to Node.js crypto algorithm name
switch (algorithm) {
case "RS256":
verifyAlgorithm = "RSA-SHA256";
break;
case "RS384":
verifyAlgorithm = "RSA-SHA384";
break;
case "RS512":
verifyAlgorithm = "RSA-SHA512";
break;
case "ES256":
verifyAlgorithm = "SHA256";
break;
case "ES384":
verifyAlgorithm = "SHA384";
break;
case "ES512":
verifyAlgorithm = "SHA512";
break;
default:
throw new Error(`Unsupported algorithm: ${algorithm}`);
}
// Convert base64url signature to standard base64
const base64Signature = base64URLToBase64(signature);
// Verify the signature
const verifier = crypto.createVerify(verifyAlgorithm);
verifier.update(input);
return verifier.verify(publicKey, base64Signature, "base64");
}
/**
* Converts base64url format to standard base64
*/
function base64URLToBase64(base64url: string): string {
// Add padding if needed
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const pad = base64.length % 4;
if (pad) {
if (pad === 1) {
throw new Error("Invalid base64url string");
}
base64 += "=".repeat(4 - pad);
}
return base64;
}
export { validateJWT };

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