Compare commits
8 commits
21f1326045
...
7098c5e513
Author | SHA1 | Date | |
---|---|---|---|
7098c5e513 | |||
9bed44dcfd | |||
13f2054ab0 | |||
0b4c4a5d4a | |||
13cc20e07b | |||
49bba46c72 | |||
5670cc8e63 | |||
31595738b8 |
134 changed files with 1673 additions and 8946 deletions
|
@ -2,8 +2,7 @@ FROM node:20-alpine AS builder
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package.json ./
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
@ -21,7 +20,7 @@ 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 && npm cache clean --force
|
||||
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
|
30
LICENSE
30
LICENSE
|
@ -1,35 +1,5 @@
|
|||
Copyright (c) 2025 Fossorial, LLC.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All files that include a header specifying they are licensed under the
|
||||
"Fossorial Commercial License" are governed by the Fossorial Commercial
|
||||
License terms. The specific terms applicable to each customer depend on the
|
||||
commercial license tier agreed upon in writing with Fossorial LLC.
|
||||
Unauthorized use, copying, modification, or distribution is strictly
|
||||
prohibited.
|
||||
|
||||
* All files that include a header specifying they are licensed under the GNU
|
||||
Affero General Public License, Version 3 ("AGPL-3"), are governed by the
|
||||
AGPL-3 terms. A full copy of the AGPL-3 license is provided below. However,
|
||||
these files are also available under the Fossorial Commercial License if a
|
||||
separate commercial license agreement has been executed between the customer
|
||||
and Fossorial LLC.
|
||||
|
||||
* All files without a license header are, by default, licensed under the GNU
|
||||
Affero General Public License, Version 3 (AGPL-3). These files may also be
|
||||
made available under the Fossorial Commercial License upon agreement with
|
||||
Fossorial LLC.
|
||||
|
||||
* All third-party components included in this repository are licensed under
|
||||
their respective original licenses, as provided by their authors.
|
||||
|
||||
Please consult the header of each individual file to determine the applicable
|
||||
license. For AGPL-3 licensed files, dual-licensing under the Fossorial
|
||||
Commercial License is available subject to written agreement with Fossorial
|
||||
LLC.
|
||||
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
|
|
|
@ -38,6 +38,9 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and access
|
|||
|
||||
_Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._
|
||||
|
||||
This is a fork of Pangolin with all proprietary code removed. Proprietary and paywalled features
|
||||
will be reimplemented under the AGPL license.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Reverse Proxy Through WireGuard Tunnel
|
||||
|
|
529
package-lock.json
generated
529
package-lock.json
generated
|
@ -383,11 +383,30 @@
|
|||
"@noble/ciphers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz",
|
||||
"integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
|
||||
"dev": true,
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
|
||||
"integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
@ -1618,6 +1637,39 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz",
|
||||
"integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.0",
|
||||
"@emnapi/runtime": "^1.4.0",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
"integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||
"integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz",
|
||||
|
@ -1828,6 +1880,38 @@
|
|||
"@node-rs/argon2-win32-x64-msvc": "2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-android-arm-eabi": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz",
|
||||
"integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-android-arm64": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz",
|
||||
"integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-darwin-arm64": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz",
|
||||
|
@ -1844,6 +1928,387 @@
|
|||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-darwin-x64": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz",
|
||||
"integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-freebsd-x64": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz",
|
||||
"integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-linux-arm-gnueabihf": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz",
|
||||
"integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-linux-arm64-gnu": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz",
|
||||
"integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-linux-arm64-musl": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz",
|
||||
"integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-linux-x64-gnu": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz",
|
||||
"integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-linux-x64-musl": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz",
|
||||
"integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-wasm32-wasi": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz",
|
||||
"integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^0.45.0",
|
||||
"@emnapi/runtime": "^0.45.0",
|
||||
"@tybys/wasm-util": "^0.8.1",
|
||||
"memfs-browser": "^3.4.13000"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
|
||||
"integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-win32-arm64-msvc": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz",
|
||||
"integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-win32-ia32-msvc": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz",
|
||||
"integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-win32-x64-msvc": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz",
|
||||
"integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-android-arm-eabi": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz",
|
||||
"integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-android-arm64": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz",
|
||||
"integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-darwin-x64": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz",
|
||||
"integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-freebsd-x64": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz",
|
||||
"integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm-gnueabihf": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz",
|
||||
"integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm64-gnu": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz",
|
||||
"integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm64-musl": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz",
|
||||
"integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-x64-musl": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz",
|
||||
"integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-wasm32-wasi": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz",
|
||||
"integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^0.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-arm64-msvc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz",
|
||||
"integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-ia32-msvc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz",
|
||||
"integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-x64-msvc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz",
|
||||
"integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/bcrypt": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz",
|
||||
|
@ -3679,6 +4144,16 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz",
|
||||
"integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz",
|
||||
|
@ -7522,6 +7997,13 @@
|
|||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-monkey": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz",
|
||||
"integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==",
|
||||
"license": "Unlicense",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
@ -9183,6 +9665,29 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
|
||||
"integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
|
||||
"license": "Unlicense",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fs-monkey": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/memfs-browser": {
|
||||
"version": "3.5.10302",
|
||||
"resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz",
|
||||
"integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==",
|
||||
"license": "Unlicense",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"memfs": "3.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
|
@ -12360,6 +12865,22 @@
|
|||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz",
|
||||
"integrity": "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userActions, roleActions, userOrgs } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
|
@ -51,6 +51,7 @@ export enum ActionsEnum {
|
|||
listRoleResources = "listRoleResources",
|
||||
// listRoleActions = "listRoleActions",
|
||||
addUserRole = "addUserRole",
|
||||
setUserRoles = "setUserRoles",
|
||||
// addUserSite = "addUserSite",
|
||||
// addUserAction = "addUserAction",
|
||||
// removeUserAction = "removeUserAction",
|
||||
|
@ -106,29 +107,28 @@ export async function checkUserActionPermission(
|
|||
}
|
||||
|
||||
try {
|
||||
let userOrgRoleId = req.userOrgRoleId;
|
||||
let userRoleIds = req.userRoleIds;
|
||||
|
||||
// If userOrgRoleId is not available on the request, fetch it
|
||||
if (userOrgRoleId === undefined) {
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
if (userRoleIds === undefined) {
|
||||
const userOrgRoles = await db
|
||||
.select({ roleId: userOrgs.roleId })
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, req.userOrgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
);
|
||||
|
||||
if (userOrgRole.length === 0) {
|
||||
if (userOrgRoles.length === 0) {
|
||||
throw createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
);
|
||||
}
|
||||
|
||||
userOrgRoleId = userOrgRole[0].roleId;
|
||||
userRoleIds = userOrgRoles.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
// Check if the user has direct permission for the action in the current org
|
||||
|
@ -155,8 +155,8 @@ export async function checkUserActionPermission(
|
|||
.where(
|
||||
and(
|
||||
eq(roleActions.actionId, actionId),
|
||||
eq(roleActions.roleId, userOrgRoleId!),
|
||||
eq(roleActions.orgId, req.userOrgId!)
|
||||
eq(roleActions.orgId, req.userOrgId!),
|
||||
inArray(roleActions.roleId, userRoleIds!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import db from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db/schemas";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId
|
||||
roleIds
|
||||
}: {
|
||||
userId: string;
|
||||
resourceId: number;
|
||||
roleId: number;
|
||||
roleIds: number[];
|
||||
}): Promise<boolean> {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
|
@ -17,7 +17,7 @@ export async function canUserAccessResource({
|
|||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
|
@ -417,15 +417,6 @@ 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 }),
|
||||
|
@ -458,12 +449,6 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
|
|||
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()
|
||||
|
@ -543,8 +528,8 @@ 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 IdpOrg = InferSelectModel<typeof idpOrg>;
|
||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
|
|
|
@ -5,8 +5,7 @@ import { createApiServer } from "./apiServer";
|
|||
import { createNextServer } from "./nextServer";
|
||||
import { createInternalServer } from "./internalServer";
|
||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas";
|
||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
import license from "./license/license.js";
|
||||
// import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
|
||||
async function startServers() {
|
||||
await runSetupFunctions();
|
||||
|
@ -17,9 +16,7 @@ async function startServers() {
|
|||
const nextServer = await createNextServer();
|
||||
|
||||
let integrationServer;
|
||||
if (await license.isUnlocked()) {
|
||||
integrationServer = createIntegrationApiServer();
|
||||
}
|
||||
// integrationServer = createIntegrationApiServer();
|
||||
|
||||
return {
|
||||
apiServer,
|
||||
|
@ -38,7 +35,7 @@ declare global {
|
|||
session?: Session;
|
||||
userOrg?: UserOrg;
|
||||
apiKeyOrg?: ApiKeyOrg;
|
||||
userOrgRoleId?: number;
|
||||
userRoleIds?: number[];
|
||||
userOrgId?: string;
|
||||
userOrgIds?: string[];
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
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,
|
||||
verifyValidLicense
|
||||
} 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();
|
||||
|
||||
apiServer.use(verifyValidLicense);
|
||||
|
||||
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" }]
|
||||
});
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import db from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db/schemas";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId
|
||||
roleIds
|
||||
}: {
|
||||
userId: string;
|
||||
resourceId: number;
|
||||
roleId: number;
|
||||
roleIds: number[];
|
||||
}): Promise<boolean> {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
|
@ -17,7 +17,7 @@ export async function canUserAccessResource({
|
|||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
|
@ -10,10 +10,6 @@ import {
|
|||
} from "@server/lib/consts";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import stoi from "./stoi";
|
||||
import db from "@server/db";
|
||||
import { SupporterKey, supporterKey } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { license } from "@server/license/license";
|
||||
|
||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||
|
||||
|
@ -172,10 +168,6 @@ const configSchema = z.object({
|
|||
export class Config {
|
||||
private rawConfig!: z.infer<typeof configSchema>;
|
||||
|
||||
supporterData: SupporterKey | null = null;
|
||||
|
||||
supporterHiddenUntil: number | null = null;
|
||||
|
||||
isDev: boolean = process.env.ENVIRONMENT !== "prod";
|
||||
|
||||
constructor() {
|
||||
|
@ -264,20 +256,9 @@ export class Config {
|
|||
: "false";
|
||||
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
|
||||
|
||||
license.setServerSecret(parsedConfig.data.server.secret);
|
||||
|
||||
this.checkKeyStatus();
|
||||
|
||||
this.rawConfig = parsedConfig.data;
|
||||
}
|
||||
|
||||
private async checkKeyStatus() {
|
||||
const licenseStatus = await license.check();
|
||||
if (!licenseStatus.isHostLicensed) {
|
||||
this.checkSupporterKey();
|
||||
}
|
||||
}
|
||||
|
||||
public getRawConfig() {
|
||||
return this.rawConfig;
|
||||
}
|
||||
|
@ -291,90 +272,6 @@ export class Config {
|
|||
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 {
|
||||
const response = await fetch(
|
||||
"https://api.fossorial.io/api/v1/license/validate",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
licenseKey,
|
||||
githubUsername
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
this.supporterData = key;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data.valid) {
|
||||
this.supporterData = {
|
||||
...key,
|
||||
valid: false
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.supporterData = {
|
||||
...key,
|
||||
tier: data.data.tier,
|
||||
valid: true
|
||||
};
|
||||
|
||||
// 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) {
|
||||
this.supporterData = key;
|
||||
console.error("Failed to validate supporter key", e);
|
||||
}
|
||||
}
|
||||
|
||||
public getSupporterData() {
|
||||
return this.supporterData;
|
||||
}
|
||||
}
|
||||
|
||||
export const config = new Config();
|
||||
|
|
|
@ -1,493 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import db from "@server/db";
|
||||
import { hostMeta, licenseKey, sites } from "@server/db/schemas";
|
||||
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;
|
|
@ -1,114 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
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 };
|
|
@ -16,7 +16,6 @@ export * from "./verifyUserInRole";
|
|||
export * from "./verifyAccessTokenAccess";
|
||||
export * from "./verifyUserIsServerAdmin";
|
||||
export * from "./verifyIsLoggedInUser";
|
||||
export * from "./integration";
|
||||
export * from "./verifyValidLicense";
|
||||
// export * from "./integration";
|
||||
export * from "./verifyUserHasAction";
|
||||
export * from "./verifyApiKeyAccess";
|
||||
// export * from "./verifyApiKeyAccess";
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
export * from "./verifyApiKey";
|
||||
export * from "./verifyApiKeyOrgAccess";
|
||||
export * from "./verifyApiKeyHasAction";
|
||||
export * from "./verifyApiKeySiteAccess";
|
||||
export * from "./verifyApiKeyResourceAccess";
|
||||
export * from "./verifyApiKeyTargetAccess";
|
||||
export * from "./verifyApiKeyRoleAccess";
|
||||
export * from "./verifyApiKeyUserAccess";
|
||||
export * from "./verifyApiKeySetResourceUsers";
|
||||
export * from "./verifyAccessTokenAccess";
|
||||
export * from "./verifyApiKeyIsRoot";
|
||||
export * from "./verifyApiKeyApiKeyAccess";
|
|
@ -1,115 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyAccessTokenAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const accessTokenId = req.params.accessTokenId;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
const [accessToken] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
||||
.limit(1);
|
||||
|
||||
if (!accessToken) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Access token with ID ${accessTokenId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resourceId = accessToken.resourceId;
|
||||
|
||||
if (!resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Access token with ID ${accessTokenId} does not have a resource ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Resource with ID ${resourceId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the API key is linked to the resource's organization
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgResult = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, resource.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (apiKeyOrgResult.length > 0) {
|
||||
req.apiKeyOrg = apiKeyOrgResult[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying access token access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import db from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
export async function verifyApiKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
const authHeader = req.headers["authorization"];
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "API key required")
|
||||
);
|
||||
}
|
||||
|
||||
const key = authHeader.split(" ")[1]; // Get the token part after "Bearer"
|
||||
const [apiKeyId, apiKeySecret] = key.split(".");
|
||||
|
||||
const [apiKey] = await db
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
||||
.limit(1);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key")
|
||||
);
|
||||
}
|
||||
|
||||
const secretHash = apiKey.apiKeyHash;
|
||||
const valid = await verifyPassword(apiKeySecret, secretHash);
|
||||
|
||||
if (!valid) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key")
|
||||
);
|
||||
}
|
||||
|
||||
req.apiKey = apiKey;
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred checking API key"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys, apiKeyOrg } from "@server/db/schemas";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyApiKeyAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const {apiKey: callerApiKey } = req;
|
||||
|
||||
const apiKeyId =
|
||||
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
|
||||
const orgId = req.params.orgId;
|
||||
|
||||
if (!callerApiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKeyId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
||||
);
|
||||
}
|
||||
|
||||
const [callerApiKeyOrg] = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!callerApiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`API key with ID ${apiKeyId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [otherApiKeyOrg] = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!otherApiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`API key with ID ${apiKeyId} does not have access to organization with ID ${orgId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying key access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import db from "@server/db";
|
||||
import { apiKeyActions } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export function verifyApiKeyHasAction(action: ActionsEnum) {
|
||||
return async function (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
if (!req.apiKey) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"API Key not authenticated"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [actionRes] = await db
|
||||
.select()
|
||||
.from(apiKeyActions)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyActions.apiKeyId, req.apiKey.apiKeyId),
|
||||
eq(apiKeyActions.actionId, action)
|
||||
)
|
||||
);
|
||||
|
||||
if (!actionRes) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error("Error verifying key action access:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying key action access"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
export async function verifyApiKeyIsRoot(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { apiKey } = req;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKey.isRoot) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have root access"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred checking API key"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function verifyApiKeyOrgAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKeyId = req.apiKey?.apiKeyId;
|
||||
const orgId = req.params.orgId;
|
||||
|
||||
if (!apiKeyId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgRes = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKeyId),
|
||||
eq(apiKeyOrg.orgId, orgId)
|
||||
)
|
||||
);
|
||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying organization access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resources, apiKeyOrg } from "@server/db/schemas";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyResourceAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const apiKey = req.apiKey;
|
||||
const resourceId =
|
||||
req.params.resourceId || req.body.resourceId || req.query.resourceId;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve the resource
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Resource with ID ${resourceId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the API key is linked to the resource's organization
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgResult = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, resource.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (apiKeyOrgResult.length > 0) {
|
||||
req.apiKeyOrg = apiKeyOrgResult[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying resource access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { roles, apiKeyOrg } from "@server/db/schemas";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function verifyApiKeyRoleAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const singleRoleId = parseInt(
|
||||
req.params.roleId || req.body.roleId || req.query.roleId
|
||||
);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
const { roleIds } = req.body;
|
||||
const allRoleIds =
|
||||
roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
||||
|
||||
if (allRoleIds.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const rolesData = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(inArray(roles.roleId, allRoleIds));
|
||||
|
||||
if (rolesData.length !== allRoleIds.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"One or more roles not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgIds = new Set(rolesData.map((role) => role.orgId));
|
||||
|
||||
for (const role of rolesData) {
|
||||
const apiKeyOrgAccess = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, role.orgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (apiKeyOrgAccess.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Key does not have access to organization for role ID ${role.roleId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (orgIds.size > 1) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Roles must belong to the same organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgId = orgIds.values().next().value;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Roles do not have an organization ID"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
// Retrieve the API key's organization link if not already set
|
||||
const apiKeyOrgRes = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (apiKeyOrgRes.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error("Error verifying role access:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying role access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeySetResourceUsers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const apiKey = req.apiKey;
|
||||
const userIds = req.body.userIds;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!userIds) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
||||
}
|
||||
|
||||
if (userIds.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const orgId = req.apiKeyOrg.orgId;
|
||||
const userOrgsData = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
inArray(userOrgs.userId, userIds),
|
||||
eq(userOrgs.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (userOrgsData.length !== userIds.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to one or more specified users"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error checking if key has access to the specified users"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
sites,
|
||||
apiKeyOrg
|
||||
} from "@server/db/schemas";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeySiteAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const siteId = parseInt(
|
||||
req.params.siteId || req.body.siteId || req.query.siteId
|
||||
);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (isNaN(siteId)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID")
|
||||
);
|
||||
}
|
||||
|
||||
const site = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (site.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!site[0].orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Site with ID ${siteId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgRes = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, site[0].orgId)
|
||||
)
|
||||
);
|
||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying site access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resources, targets, apiKeyOrg } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyTargetAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const targetId = parseInt(req.params.targetId);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (isNaN(targetId)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID")
|
||||
);
|
||||
}
|
||||
|
||||
const [target] = await db
|
||||
.select()
|
||||
.from(targets)
|
||||
.where(eq(targets.targetId, targetId))
|
||||
.limit(1);
|
||||
|
||||
if (!target) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Target with ID ${targetId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resourceId = target.resourceId;
|
||||
if (!resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Target with ID ${targetId} does not have a resource ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Resource with ID ${resourceId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgResult = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, resource.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (apiKeyOrgResult.length > 0) {
|
||||
req.apiKeyOrg = apiKeyOrgResult[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying target access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyUserAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const reqUserId =
|
||||
req.params.userId || req.body.userId || req.query.userId;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!reqUserId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have organization access"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgId = req.apiKeyOrg.orgId;
|
||||
|
||||
const [userOrgRecord] = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!userOrgRecord) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this user"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error checking if key has access to this user"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -82,24 +82,24 @@ export async function verifyAccessTokenAccess(
|
|||
)
|
||||
);
|
||||
req.userOrg = res[0];
|
||||
req.userRoleIds = res.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
next(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
req.userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgId = resource[0].orgId!;
|
||||
}
|
||||
|
||||
req.userOrgId = resource[0].orgId!;
|
||||
|
||||
const resourceAllowed = await canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId: req.userOrgRoleId!
|
||||
roleIds: req.userRoleIds!
|
||||
});
|
||||
|
||||
if (!resourceAllowed) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userOrgs } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
|
@ -29,9 +29,11 @@ export async function verifyAdmin(
|
|||
const userOrgRes = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)))
|
||||
.limit(1);
|
||||
.where(
|
||||
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!))
|
||||
);
|
||||
req.userOrg = userOrgRes[0];
|
||||
req.userRoleIds = userOrgRes.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
|
@ -43,13 +45,13 @@ export async function verifyAdmin(
|
|||
);
|
||||
}
|
||||
|
||||
const userRole = await db
|
||||
const userAdminRole = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, req.userOrg.roleId))
|
||||
.where(and(inArray(roles.roleId, req.userRoleIds!), roles.isAdmin))
|
||||
.limit(1);
|
||||
|
||||
if (userRole.length === 0 || !userRole[0].isAdmin) {
|
||||
if (userAdminRole.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const apiKeyId =
|
||||
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
|
||||
const orgId = req.params.orgId;
|
||||
|
||||
if (!userId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKeyId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
||||
);
|
||||
}
|
||||
|
||||
const [apiKey] = await db
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.innerJoin(apiKeyOrg, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId))
|
||||
.where(
|
||||
and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!apiKey.apiKeys) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`API key with ID ${apiKeyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKeyOrg.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`API key with ID ${apiKeyId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, apiKeyOrg.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
req.userOrg = userOrgRole[0];
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgRoleId = userOrgRoleId;
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying key access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -34,21 +34,20 @@ export async function verifyOrgAccess(
|
|||
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||
);
|
||||
req.userOrg = userOrgRes[0];
|
||||
req.userRoleIds = userOrgRes.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
next(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// User has access, attach the user's role to the request for potential future use
|
||||
req.userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgId = orgId;
|
||||
return next();
|
||||
}
|
||||
|
||||
req.userOrgId = orgId;
|
||||
return next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
|
|
@ -4,9 +4,9 @@ import {
|
|||
resources,
|
||||
userOrgs,
|
||||
userResources,
|
||||
roleResources,
|
||||
roleResources
|
||||
} from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
|
@ -59,9 +59,9 @@ export async function verifyResourceAccess(
|
|||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, resource[0].orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
);
|
||||
req.userOrg = userOrgRole[0];
|
||||
req.userRoleIds = userOrgRole.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
|
@ -73,8 +73,6 @@ export async function verifyResourceAccess(
|
|||
);
|
||||
}
|
||||
|
||||
const userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgRoleId = userOrgRoleId;
|
||||
req.userOrgId = resource[0].orgId;
|
||||
|
||||
const roleResourceAccess = await db
|
||||
|
@ -83,7 +81,7 @@ export async function verifyResourceAccess(
|
|||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, userOrgRoleId)
|
||||
inArray(roleResources.roleId, req.userRoleIds!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
|
@ -98,11 +98,10 @@ export async function verifyRoleAccess(
|
|||
.from(userOrgs)
|
||||
.where(
|
||||
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!))
|
||||
)
|
||||
.limit(1);
|
||||
);
|
||||
|
||||
req.userOrg = userOrg[0];
|
||||
req.userOrgRoleId = userOrg[0].roleId;
|
||||
req.userRoleIds = userOrg.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
return next();
|
||||
|
|
|
@ -5,9 +5,9 @@ import {
|
|||
userOrgs,
|
||||
userSites,
|
||||
roleSites,
|
||||
roles,
|
||||
roles
|
||||
} from "@server/db/schemas";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
|
@ -71,6 +71,7 @@ export async function verifySiteAccess(
|
|||
)
|
||||
.limit(1);
|
||||
req.userOrg = userOrgRole[0];
|
||||
req.userRoleIds = userOrgRole.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
|
@ -82,8 +83,6 @@ export async function verifySiteAccess(
|
|||
);
|
||||
}
|
||||
|
||||
const userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgRoleId = userOrgRoleId;
|
||||
req.userOrgId = site[0].orgId;
|
||||
|
||||
// Check role-based site access first
|
||||
|
@ -93,7 +92,7 @@ export async function verifySiteAccess(
|
|||
.where(
|
||||
and(
|
||||
eq(roleSites.siteId, siteId),
|
||||
eq(roleSites.roleId, userOrgRoleId)
|
||||
inArray(roleSites.roleId, req.userRoleIds!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
|
@ -88,24 +88,23 @@ export async function verifyTargetAccess(
|
|||
)
|
||||
);
|
||||
req.userOrg = res[0];
|
||||
req.userRoleIds = res.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
next(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
req.userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgId = resource[0].orgId!;
|
||||
}
|
||||
req.userOrgId = resource[0].orgId!;
|
||||
|
||||
const resourceAllowed = await canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId: req.userOrgRoleId!
|
||||
roleIds: req.userRoleIds!
|
||||
});
|
||||
|
||||
if (!resourceAllowed) {
|
||||
|
|
|
@ -33,9 +33,9 @@ export async function verifyUserAccess(
|
|||
eq(userOrgs.userId, reqUserId),
|
||||
eq(userOrgs.orgId, req.userOrgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
);
|
||||
req.userOrg = res[0];
|
||||
req.userRoleIds = res.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
|
|
|
@ -12,7 +12,7 @@ export async function verifyUserInRole(
|
|||
const roleId = parseInt(
|
||||
req.params.roleId || req.body.roleId || req.query.roleId
|
||||
);
|
||||
const userRoleId = req.userOrgRoleId;
|
||||
const userRoleIds = req.userRoleIds;
|
||||
|
||||
if (isNaN(roleId)) {
|
||||
return next(
|
||||
|
@ -20,7 +20,7 @@ export async function verifyUserInRole(
|
|||
);
|
||||
}
|
||||
|
||||
if (!userRoleId) {
|
||||
if (!userRoleIds) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -29,7 +29,7 @@ export async function verifyUserInRole(
|
|||
);
|
||||
}
|
||||
|
||||
if (userRoleId !== roleId) {
|
||||
if (userRoleIds.indexOf(roleId) === -1) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
|
|
@ -36,6 +36,7 @@ export async function verifyUserIsOrgOwner(
|
|||
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||
);
|
||||
req.userOrg = res[0];
|
||||
req.userRoleIds = res.map((r) => r.roleId);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import license from "@server/license/license";
|
||||
|
||||
export async function verifyValidLicense(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const unlocked = await license.isUnlocked();
|
||||
if (!unlocked) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "License is not valid")
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying license"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -208,7 +208,7 @@ export async function listAccessTokens(
|
|||
.where(
|
||||
or(
|
||||
eq(userResources.userId, req.user!.userId),
|
||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
inArray(roleResources.roleId, req.userRoleIds!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
import moment from "moment";
|
||||
import {
|
||||
generateId,
|
||||
generateIdFromEntropySize
|
||||
} from "@server/auth/sessions/app";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
orgId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const bodySchema = z.object({
|
||||
name: z.string().min(1).max(255)
|
||||
});
|
||||
|
||||
export type CreateOrgApiKeyBody = z.infer<typeof bodySchema>;
|
||||
|
||||
export type CreateOrgApiKeyResponse = {
|
||||
apiKeyId: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
lastChars: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/api-key",
|
||||
description: "Create a new API key scoped to the organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: bodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createOrgApiKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const { name } = parsedBody.data;
|
||||
|
||||
const apiKeyId = generateId(15);
|
||||
const apiKey = generateIdFromEntropySize(25);
|
||||
const apiKeyHash = await hashPassword(apiKey);
|
||||
const lastChars = apiKey.slice(-4);
|
||||
const createdAt = moment().toISOString();
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(apiKeys).values({
|
||||
name,
|
||||
apiKeyId,
|
||||
apiKeyHash,
|
||||
createdAt,
|
||||
lastChars
|
||||
});
|
||||
|
||||
await trx.insert(apiKeyOrg).values({
|
||||
apiKeyId,
|
||||
orgId
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
return response<CreateOrgApiKeyResponse>(res, {
|
||||
data: {
|
||||
apiKeyId,
|
||||
apiKey,
|
||||
name,
|
||||
lastChars,
|
||||
createdAt
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API key created",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create API key"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { apiKeyOrg, apiKeys, orgs } from "@server/db/schemas";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
import moment from "moment";
|
||||
import {
|
||||
generateId,
|
||||
generateIdFromEntropySize
|
||||
} from "@server/auth/sessions/app";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255)
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateRootApiKeyBody = z.infer<typeof bodySchema>;
|
||||
|
||||
export type CreateRootApiKeyResponse = {
|
||||
apiKeyId: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
lastChars: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export async function createRootApiKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { name } = parsedBody.data;
|
||||
|
||||
const apiKeyId = generateId(15);
|
||||
const apiKey = generateIdFromEntropySize(25);
|
||||
const apiKeyHash = await hashPassword(apiKey);
|
||||
const lastChars = apiKey.slice(-4);
|
||||
const createdAt = moment().toISOString();
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(apiKeys).values({
|
||||
apiKeyId,
|
||||
name,
|
||||
apiKeyHash,
|
||||
createdAt,
|
||||
lastChars,
|
||||
isRoot: true
|
||||
});
|
||||
|
||||
const allOrgs = await trx.select().from(orgs);
|
||||
|
||||
for (const org of allOrgs) {
|
||||
await trx.insert(apiKeyOrg).values({
|
||||
apiKeyId,
|
||||
orgId: org.orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return response<CreateRootApiKeyResponse>(res, {
|
||||
data: {
|
||||
apiKeyId,
|
||||
name,
|
||||
apiKey,
|
||||
lastChars,
|
||||
createdAt
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API key created",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create API key"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/org/{orgId}/api-key/{apiKeyId}",
|
||||
description: "Delete an API key.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteApiKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { apiKeyId } = parsedParams.data;
|
||||
|
||||
const [apiKey] = await db
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
||||
.limit(1);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`API Key with ID ${apiKeyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API key deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty(),
|
||||
orgId: z.string().nonempty()
|
||||
});
|
||||
|
||||
export async function deleteOrgApiKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { apiKeyId, orgId } = parsedParams.data;
|
||||
|
||||
const [apiKey] = await db
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
||||
.innerJoin(
|
||||
apiKeyOrg,
|
||||
and(
|
||||
eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`API Key with ID ${apiKeyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (apiKey.apiKeys.isRoot) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot delete root API key"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKeyId),
|
||||
eq(apiKeyOrg.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
const apiKeyOrgs = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(eq(apiKeyOrg.apiKeyId, apiKeyId));
|
||||
|
||||
if (apiKeyOrgs.length === 0) {
|
||||
await trx.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId));
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API removed from organization",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
async function query(apiKeyId: string) {
|
||||
return await db
|
||||
.select({
|
||||
apiKeyId: apiKeys.apiKeyId,
|
||||
lastChars: apiKeys.lastChars,
|
||||
createdAt: apiKeys.createdAt,
|
||||
isRoot: apiKeys.isRoot,
|
||||
name: apiKeys.name
|
||||
})
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
export type GetApiKeyResponse = NonNullable<
|
||||
Awaited<ReturnType<typeof query>>[0]
|
||||
>;
|
||||
|
||||
export async function getApiKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { apiKeyId } = parsedParams.data;
|
||||
|
||||
const [apiKey] = await query(apiKeyId);
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`API Key with ID ${apiKeyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<GetApiKeyResponse>(res, {
|
||||
data: apiKey,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API key deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
export * from "./createRootApiKey";
|
||||
export * from "./deleteApiKey";
|
||||
export * from "./getApiKey";
|
||||
export * from "./listApiKeyActions";
|
||||
export * from "./listOrgApiKeys";
|
||||
export * from "./listApiKeyActions";
|
||||
export * from "./listRootApiKeys";
|
||||
export * from "./setApiKeyActions";
|
||||
export * from "./setApiKeyOrgs";
|
||||
export * from "./createOrgApiKey";
|
||||
export * from "./deleteOrgApiKey";
|
|
@ -1,118 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { db } from "@server/db";
|
||||
import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const querySchema = z.object({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
function queryActions(apiKeyId: string) {
|
||||
return db
|
||||
.select({
|
||||
actionId: actions.actionId
|
||||
})
|
||||
.from(apiKeyActions)
|
||||
.where(eq(apiKeyActions.apiKeyId, apiKeyId))
|
||||
.innerJoin(actions, eq(actions.actionId, apiKeyActions.actionId));
|
||||
}
|
||||
|
||||
export type ListApiKeyActionsResponse = {
|
||||
actions: Awaited<ReturnType<typeof queryActions>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
|
||||
description:
|
||||
"List all actions set for an API key.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
query: querySchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listApiKeyActions(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
const { apiKeyId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryActions(apiKeyId);
|
||||
|
||||
const actionsList = await baseQuery.limit(limit).offset(offset);
|
||||
|
||||
return response<ListApiKeyActionsResponse>(res, {
|
||||
data: {
|
||||
actions: actionsList,
|
||||
pagination: {
|
||||
total: actionsList.length,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API keys retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const querySchema = z.object({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
const paramsSchema = z.object({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
function queryApiKeys(orgId: string) {
|
||||
return db
|
||||
.select({
|
||||
apiKeyId: apiKeys.apiKeyId,
|
||||
orgId: apiKeyOrg.orgId,
|
||||
lastChars: apiKeys.lastChars,
|
||||
createdAt: apiKeys.createdAt,
|
||||
name: apiKeys.name
|
||||
})
|
||||
.from(apiKeyOrg)
|
||||
.where(and(eq(apiKeyOrg.orgId, orgId), eq(apiKeys.isRoot, false)))
|
||||
.innerJoin(apiKeys, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId));
|
||||
}
|
||||
|
||||
export type ListOrgApiKeysResponse = {
|
||||
apiKeys: Awaited<ReturnType<typeof queryApiKeys>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/api-keys",
|
||||
description: "List all API keys for an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
query: querySchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listOrgApiKeys(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryApiKeys(orgId);
|
||||
|
||||
const apiKeysList = await baseQuery.limit(limit).offset(offset);
|
||||
|
||||
return response<ListOrgApiKeysResponse>(res, {
|
||||
data: {
|
||||
apiKeys: apiKeysList,
|
||||
pagination: {
|
||||
total: apiKeysList.length,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API keys retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { db } from "@server/db";
|
||||
import { apiKeys } from "@server/db/schemas";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const querySchema = z.object({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
function queryApiKeys() {
|
||||
return db
|
||||
.select({
|
||||
apiKeyId: apiKeys.apiKeyId,
|
||||
lastChars: apiKeys.lastChars,
|
||||
createdAt: apiKeys.createdAt,
|
||||
name: apiKeys.name
|
||||
})
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.isRoot, true));
|
||||
}
|
||||
|
||||
export type ListRootApiKeysResponse = {
|
||||
apiKeys: Awaited<ReturnType<typeof queryApiKeys>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
export async function listRootApiKeys(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
const baseQuery = queryApiKeys();
|
||||
|
||||
const apiKeysList = await baseQuery.limit(limit).offset(offset);
|
||||
|
||||
return response<ListRootApiKeysResponse>(res, {
|
||||
data: {
|
||||
apiKeys: apiKeysList,
|
||||
pagination: {
|
||||
total: apiKeysList.length,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API keys retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { actions, apiKeyActions } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
actionIds: z
|
||||
.array(z.string().nonempty())
|
||||
.transform((v) => Array.from(new Set(v)))
|
||||
})
|
||||
.strict();
|
||||
|
||||
const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
|
||||
description:
|
||||
"Set actions for an API key. This will replace any existing actions.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: bodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setApiKeyActions(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { actionIds: newActionIds } = parsedBody.data;
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { apiKeyId } = parsedParams.data;
|
||||
|
||||
const actionsExist = await db
|
||||
.select()
|
||||
.from(actions)
|
||||
.where(inArray(actions.actionId, newActionIds));
|
||||
|
||||
if (actionsExist.length !== newActionIds.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"One or more actions do not exist"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const existingActions = await trx
|
||||
.select()
|
||||
.from(apiKeyActions)
|
||||
.where(eq(apiKeyActions.apiKeyId, apiKeyId));
|
||||
|
||||
const existingActionIds = existingActions.map((a) => a.actionId);
|
||||
|
||||
const actionIdsToAdd = newActionIds.filter(
|
||||
(id) => !existingActionIds.includes(id)
|
||||
);
|
||||
const actionIdsToRemove = existingActionIds.filter(
|
||||
(id) => !newActionIds.includes(id)
|
||||
);
|
||||
|
||||
if (actionIdsToRemove.length > 0) {
|
||||
await trx
|
||||
.delete(apiKeyActions)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyActions.apiKeyId, apiKeyId),
|
||||
inArray(apiKeyActions.actionId, actionIdsToRemove)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (actionIdsToAdd.length > 0) {
|
||||
const insertValues = actionIdsToAdd.map((actionId) => ({
|
||||
apiKeyId,
|
||||
actionId
|
||||
}));
|
||||
await trx.insert(apiKeyActions).values(insertValues);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API key actions updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyOrg, orgs } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
orgIds: z
|
||||
.array(z.string().nonempty())
|
||||
.transform((v) => Array.from(new Set(v)))
|
||||
})
|
||||
.strict();
|
||||
|
||||
const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
export async function setApiKeyOrgs(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgIds: newOrgIds } = parsedBody.data;
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { apiKeyId } = parsedParams.data;
|
||||
|
||||
// make sure all orgs exist
|
||||
const allOrgs = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(inArray(orgs.orgId, newOrgIds));
|
||||
|
||||
if (allOrgs.length !== newOrgIds.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"One or more orgs do not exist"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const existingOrgs = await trx
|
||||
.select({ orgId: apiKeyOrg.orgId })
|
||||
.from(apiKeyOrg)
|
||||
.where(eq(apiKeyOrg.apiKeyId, apiKeyId));
|
||||
|
||||
const existingOrgIds = existingOrgs.map((a) => a.orgId);
|
||||
|
||||
const orgIdsToAdd = newOrgIds.filter(
|
||||
(id) => !existingOrgIds.includes(id)
|
||||
);
|
||||
const orgIdsToRemove = existingOrgIds.filter(
|
||||
(id) => !newOrgIds.includes(id)
|
||||
);
|
||||
|
||||
if (orgIdsToRemove.length > 0) {
|
||||
await trx
|
||||
.delete(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKeyId),
|
||||
inArray(apiKeyOrg.orgId, orgIdsToRemove)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (orgIdsToAdd.length > 0) {
|
||||
const insertValues = orgIdsToAdd.map((orgId) => ({
|
||||
apiKeyId,
|
||||
orgId
|
||||
}));
|
||||
await trx.insert(apiKeyOrg).values(insertValues);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "API key orgs updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,11 +8,9 @@ import * as target from "./target";
|
|||
import * as user from "./user";
|
||||
import * as auth from "./auth";
|
||||
import * as role from "./role";
|
||||
import * as supporterKey from "./supporterKey";
|
||||
import * as accessToken from "./accessToken";
|
||||
import * as idp from "./idp";
|
||||
import * as license from "./license";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
// import * as apiKeys from "./apiKeys";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import {
|
||||
verifyAccessTokenAccess,
|
||||
|
@ -28,9 +26,8 @@ import {
|
|||
verifyUserAccess,
|
||||
getUserOrgs,
|
||||
verifyUserIsServerAdmin,
|
||||
verifyIsLoggedInUser,
|
||||
verifyApiKeyAccess,
|
||||
verifyValidLicense
|
||||
verifyIsLoggedInUser
|
||||
// verifyApiKeyAccess
|
||||
} from "@server/middlewares";
|
||||
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
|
@ -276,6 +273,14 @@ authenticated.get(
|
|||
verifyUserHasAction(ActionsEnum.listRoles),
|
||||
role.listRoles
|
||||
);
|
||||
authenticated.post(
|
||||
"/org/:orgId/user/:userId/roles",
|
||||
verifyOrgAccess,
|
||||
verifyUserAccess,
|
||||
verifyUserHasAction(ActionsEnum.setUserRoles),
|
||||
user.setUserRoles
|
||||
);
|
||||
|
||||
// authenticated.get(
|
||||
// "/role/:roleId",
|
||||
// verifyRoleAccess,
|
||||
|
@ -406,12 +411,6 @@ authenticated.get(
|
|||
|
||||
authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview);
|
||||
|
||||
authenticated.post(
|
||||
`/supporter-key/validate`,
|
||||
supporterKey.validateSupporterKey
|
||||
);
|
||||
authenticated.post(`/supporter-key/hide`, supporterKey.hideSupporterKey);
|
||||
|
||||
unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
|
||||
|
||||
// authenticated.get(
|
||||
|
@ -531,28 +530,24 @@ authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
|||
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
idp.createIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
idp.updateIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
idp.deleteIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/idp/:idpId/org",
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
idp.listIdpOrgPolicies
|
||||
);
|
||||
|
@ -560,75 +555,45 @@ authenticated.get(
|
|||
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
|
||||
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
||||
|
||||
authenticated.post(
|
||||
"/license/activate",
|
||||
verifyUserIsServerAdmin,
|
||||
license.activateLicense
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/license/keys",
|
||||
verifyUserIsServerAdmin,
|
||||
license.listLicenseKeys
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/license/:licenseKey",
|
||||
verifyUserIsServerAdmin,
|
||||
license.deleteLicenseKey
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/license/recheck",
|
||||
verifyUserIsServerAdmin,
|
||||
license.recheckStatus
|
||||
);
|
||||
|
||||
/*
|
||||
authenticated.get(
|
||||
`/api-key/:apiKeyId`,
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.getApiKey
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
`/api-key`,
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.createRootApiKey
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/api-key/:apiKeyId`,
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.deleteApiKey
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-keys`,
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.listRootApiKeys
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-key/:apiKeyId/actions`,
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.listApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/api-key/:apiKeyId/actions`,
|
||||
verifyValidLicense,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.setApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-keys`,
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listApiKeys),
|
||||
apiKeys.listOrgApiKeys
|
||||
|
@ -636,7 +601,6 @@ authenticated.get(
|
|||
|
||||
authenticated.post(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyUserHasAction(ActionsEnum.setApiKeyActions),
|
||||
|
@ -645,7 +609,6 @@ authenticated.post(
|
|||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyUserHasAction(ActionsEnum.listApiKeyActions),
|
||||
|
@ -654,7 +617,6 @@ authenticated.get(
|
|||
|
||||
authenticated.put(
|
||||
`/org/:orgId/api-key`,
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey
|
||||
|
@ -662,7 +624,6 @@ authenticated.put(
|
|||
|
||||
authenticated.delete(
|
||||
`/org/:orgId/api-key/:apiKeyId`,
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteApiKey),
|
||||
|
@ -671,12 +632,12 @@ authenticated.delete(
|
|||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-key/:apiKeyId`,
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyUserHasAction(ActionsEnum.getApiKey),
|
||||
apiKeys.getApiKey
|
||||
);
|
||||
*/
|
||||
|
||||
// Auth routes
|
||||
export const authRouter = Router();
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
|
@ -12,7 +7,6 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import config from "@server/lib/config";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { idp, idpOrg } from "@server/db/schemas";
|
||||
|
||||
|
@ -56,16 +50,6 @@ export async function createIdpOrgPolicy(
|
|||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
|
@ -75,10 +59,20 @@ export async function createIdpOrgPolicy(
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { idpId, orgId } = parsedParams.data;
|
||||
const { roleMapping, orgMapping } = parsedBody.data;
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
let { orgMapping, roleMapping } = parsedBody.data;
|
||||
|
||||
// Given identity provider must exist and not have a policy already
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
|
@ -90,18 +84,15 @@ export async function createIdpOrgPolicy(
|
|||
|
||||
if (!existing?.idp) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"An IDP with this ID does not exist."
|
||||
)
|
||||
createHttpError(HttpCode.NOT_FOUND, "Idp does not exist")
|
||||
);
|
||||
}
|
||||
|
||||
if (existing.idpOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"An IDP org policy already exists."
|
||||
HttpCode.CONFLICT,
|
||||
"Org policy already exists for this idp"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -109,15 +100,15 @@ export async function createIdpOrgPolicy(
|
|||
await db.insert(idpOrg).values({
|
||||
idpId,
|
||||
orgId,
|
||||
roleMapping,
|
||||
orgMapping
|
||||
orgMapping,
|
||||
roleMapping
|
||||
});
|
||||
|
||||
return response<CreateIdpOrgPolicyResponse>(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Idp created successfully",
|
||||
message: "Idp org policy created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -11,7 +11,6 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas";
|
|||
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import license from "@server/license/license";
|
||||
|
||||
const paramsSchema = z.object({}).strict();
|
||||
|
||||
|
@ -81,10 +80,6 @@ export async function createOidcIdp(
|
|||
autoProvision
|
||||
} = parsedBody.data;
|
||||
|
||||
if (!(await license.isUnlocked())) {
|
||||
autoProvision = false;
|
||||
}
|
||||
|
||||
const key = config.getRawConfig().server.secret;
|
||||
|
||||
const encryptedSecret = encrypt(clientSecret, key);
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
|
@ -11,7 +6,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { idp, idpOrg } from "@server/db/schemas";
|
||||
import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
|
@ -25,7 +20,7 @@ const paramsSchema = z
|
|||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/idp/{idpId}/org/{orgId}",
|
||||
description: "Create an OIDC IdP for an organization.",
|
||||
description: "Delete an IDP policy for an IDP on an organization.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
|
@ -51,26 +46,27 @@ export async function deleteIdpOrgPolicy(
|
|||
|
||||
const { idpId, orgId } = parsedParams.data;
|
||||
|
||||
// Check if IDP policy, exists
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
.leftJoin(idpOrg, eq(idpOrg.orgId, orgId))
|
||||
.where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId)));
|
||||
.leftJoin(
|
||||
idpOrg,
|
||||
and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId))
|
||||
)
|
||||
.where(eq(idp.idpId, idpId));
|
||||
|
||||
if (!existing.idp) {
|
||||
if (!existing?.idp) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"An IDP with this ID does not exist."
|
||||
)
|
||||
createHttpError(HttpCode.NOT_FOUND, "Idp does not exist")
|
||||
);
|
||||
}
|
||||
|
||||
if (!existing.idpOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"A policy for this IDP and org does not exist."
|
||||
HttpCode.NOT_FOUND,
|
||||
"Org policy does not exist for this idp"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -83,7 +79,7 @@ export async function deleteIdpOrgPolicy(
|
|||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Policy deleted successfully",
|
||||
message: "Idp policy deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -7,5 +7,5 @@ export * from "./validateOidcCallback";
|
|||
export * from "./getIdp";
|
||||
export * from "./createIdpOrgPolicy";
|
||||
export * from "./deleteIdpOrgPolicy";
|
||||
export * from "./listIdpOrgPolicies";
|
||||
export * from "./updateIdpOrgPolicy";
|
||||
export * from "./listIdpOrgPolicies";
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { idpOrg } from "@server/db/schemas";
|
||||
import { idpOrg, type IdpOrg } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
@ -15,9 +10,11 @@ import logger from "@server/logger";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
idpId: z.coerce.number()
|
||||
});
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
idpId: z.coerce.number()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const querySchema = z
|
||||
.object({
|
||||
|
@ -47,8 +44,12 @@ async function query(idpId: number, limit: number, offset: number) {
|
|||
}
|
||||
|
||||
export type ListIdpOrgPoliciesResponse = {
|
||||
policies: NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
policies: Array<IdpOrg>;
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
|
@ -78,6 +79,7 @@ export async function listIdpOrgPolicies(
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { idpId } = parsedParams.data;
|
||||
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
|
@ -109,7 +111,7 @@ export async function listIdpOrgPolicies(
|
|||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Policies retrieved successfully",
|
||||
message: "Idp org policies retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
createSession,
|
||||
generateId,
|
||||
generateSessionToken,
|
||||
serializeSessionCookie
|
||||
} from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { Idp, idpOrg, orgs, roles, User, userOrgs, users } from "@server/db/schemas";
|
||||
import logger from "@server/logger";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
Idp,
|
||||
idpOrg,
|
||||
orgs,
|
||||
roles,
|
||||
User,
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db/schemas";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import jmespath from "jmespath";
|
||||
import { Request, Response } from "express";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
const extractedRolesSchema = z.array(z.string()).or(z.string()).nullable();
|
||||
|
||||
export async function oidcAutoProvision({
|
||||
idp,
|
||||
|
@ -36,83 +42,92 @@ export async function oidcAutoProvision({
|
|||
req: Request;
|
||||
res: Response;
|
||||
}) {
|
||||
// Get user's roles of all orgs as stated in the ID token claims
|
||||
const allOrgs = await db.select().from(orgs);
|
||||
const userOrgInfo: { orgId: string; roleId: number }[] = [];
|
||||
|
||||
const defaultRoleMapping = idp.defaultRoleMapping;
|
||||
const defaultOrgMapping = idp.defaultOrgMapping;
|
||||
|
||||
let userOrgInfo: { orgId: string; roleId: number }[] = [];
|
||||
for (const org of allOrgs) {
|
||||
const [idpOrgRes] = await db
|
||||
const idpOrgs = await db
|
||||
.select()
|
||||
.from(idpOrg)
|
||||
.where(
|
||||
and(eq(idpOrg.idpId, idp.idpId), eq(idpOrg.orgId, org.orgId))
|
||||
);
|
||||
if (idpOrgs.length === 0) continue;
|
||||
const idpOrgRes = idpOrgs[0];
|
||||
|
||||
let roleId: number | undefined = undefined;
|
||||
const orgMapping = hydrateOrgMapping(
|
||||
idpOrgRes.orgMapping || idp.defaultOrgMapping,
|
||||
org.orgId
|
||||
);
|
||||
const roleMapping = idpOrgRes.roleMapping || idp.defaultRoleMapping;
|
||||
|
||||
const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping;
|
||||
const hydratedOrgMapping = hydrateOrgMapping(orgMapping, org.orgId);
|
||||
|
||||
if (hydratedOrgMapping) {
|
||||
logger.debug("Hydrated Org Mapping", {
|
||||
hydratedOrgMapping
|
||||
});
|
||||
const orgId = jmespath.search(claims, hydratedOrgMapping);
|
||||
logger.debug("Extraced Org ID", { orgId });
|
||||
if (orgMapping) {
|
||||
const orgId = jmespath.search(claims, orgMapping);
|
||||
logger.debug("Extracted org ID", { orgId });
|
||||
if (orgId !== true && orgId !== org.orgId) {
|
||||
// user not allowed to access this org
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const roleMapping = idpOrgRes?.roleMapping || defaultRoleMapping;
|
||||
if (roleMapping) {
|
||||
logger.debug("Role Mapping", { roleMapping });
|
||||
const roleName = jmespath.search(claims, roleMapping);
|
||||
|
||||
if (!roleName) {
|
||||
logger.error("Role name not found in the ID token", {
|
||||
roleName
|
||||
logger.info("claims", { claims });
|
||||
const extractedRoles = extractedRolesSchema.safeParse(
|
||||
jmespath.search(claims, roleMapping)
|
||||
);
|
||||
if (!extractedRoles.success) {
|
||||
logger.error("Error extracting roles", {
|
||||
error: extractedRoles.error
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const rd = extractedRoles.data;
|
||||
if (!rd) {
|
||||
continue;
|
||||
}
|
||||
const rolesFromToken = typeof rd === "string" ? [rd] : rd;
|
||||
logger.debug("Extracted roles", { rolesFromToken });
|
||||
if (rd.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [roleRes] = await db
|
||||
const rolesFromDb = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(eq(roles.orgId, org.orgId), eq(roles.name, roleName))
|
||||
and(
|
||||
eq(roles.orgId, org.orgId),
|
||||
inArray(roles.name, rolesFromToken)
|
||||
)
|
||||
);
|
||||
|
||||
if (!roleRes) {
|
||||
logger.error("Role not found", {
|
||||
orgId: org.orgId,
|
||||
roleName
|
||||
});
|
||||
if (rolesFromDb.length === 0) {
|
||||
logger.error("Role(s) not found", { roles: rolesFromToken });
|
||||
continue;
|
||||
}
|
||||
if (rolesFromDb.length < rolesFromToken.length) {
|
||||
logger.warn("Role(s) not found", {
|
||||
roles: rolesFromToken.filter(
|
||||
(r) => !rolesFromDb.some((rdb) => rdb.name === r)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
roleId = roleRes.roleId;
|
||||
|
||||
userOrgInfo.push({
|
||||
orgId: org.orgId,
|
||||
roleId
|
||||
rolesFromDb.forEach((r) => {
|
||||
userOrgInfo.push({
|
||||
orgId: org.orgId,
|
||||
roleId: r.roleId
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("User org info", { userOrgInfo });
|
||||
|
||||
let existingUserId = existingUser?.userId;
|
||||
|
||||
let userId = existingUser?.userId;
|
||||
// sync the user with the orgs and roles
|
||||
await db.transaction(async (trx) => {
|
||||
let userId = existingUser?.userId;
|
||||
|
||||
// create user if not exists
|
||||
if (!existingUser) {
|
||||
if (!userId) {
|
||||
// create user if it does not exist
|
||||
userId = generateId(15);
|
||||
|
||||
await trx.insert(users).values({
|
||||
|
@ -126,7 +141,7 @@ export async function oidcAutoProvision({
|
|||
dateCreated: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
// set the name and email
|
||||
// update username/email
|
||||
await trx
|
||||
.update(users)
|
||||
.set({
|
||||
|
@ -134,84 +149,60 @@ export async function oidcAutoProvision({
|
|||
email: email || null,
|
||||
name: name || null
|
||||
})
|
||||
.where(eq(users.userId, userId!));
|
||||
.where(eq(users.userId, userId));
|
||||
}
|
||||
|
||||
existingUserId = userId;
|
||||
|
||||
// get all current user orgs
|
||||
// get all current user orgs/roles
|
||||
const currentUserOrgs = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.userId, userId!));
|
||||
.where(eq(userOrgs.userId, userId));
|
||||
|
||||
// Delete orgs that are no longer valid
|
||||
const orgsToDelete = currentUserOrgs.filter(
|
||||
(currentOrg) =>
|
||||
!userOrgInfo.some((newOrg) => newOrg.orgId === currentOrg.orgId)
|
||||
);
|
||||
const orgsToDelete = currentUserOrgs
|
||||
.filter(
|
||||
(currentOrg) =>
|
||||
!userOrgInfo.some(
|
||||
(newOrg) =>
|
||||
newOrg.orgId === currentOrg.orgId &&
|
||||
newOrg.roleId === currentOrg.roleId
|
||||
)
|
||||
)
|
||||
.map((org) => org.orgId);
|
||||
|
||||
if (orgsToDelete.length > 0) {
|
||||
await trx.delete(userOrgs).where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId!),
|
||||
inArray(
|
||||
userOrgs.orgId,
|
||||
orgsToDelete.map((org) => org.orgId)
|
||||
await trx
|
||||
.delete(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId!),
|
||||
inArray(userOrgs.orgId, orgsToDelete)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update roles for existing orgs where the role has changed
|
||||
const orgsToUpdate = currentUserOrgs.filter((currentOrg) => {
|
||||
const newOrg = userOrgInfo.find(
|
||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||
);
|
||||
return newOrg && newOrg.roleId !== currentOrg.roleId;
|
||||
});
|
||||
|
||||
if (orgsToUpdate.length > 0) {
|
||||
for (const org of orgsToUpdate) {
|
||||
const newRole = userOrgInfo.find(
|
||||
(newOrg) => newOrg.orgId === org.orgId
|
||||
);
|
||||
if (newRole) {
|
||||
await trx
|
||||
.update(userOrgs)
|
||||
.set({ roleId: newRole.roleId })
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId!),
|
||||
eq(userOrgs.orgId, org.orgId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add new orgs that don't exist yet
|
||||
const orgsToAdd = userOrgInfo.filter(
|
||||
(newOrg) =>
|
||||
!currentUserOrgs.some(
|
||||
(currentOrg) => currentOrg.orgId === newOrg.orgId
|
||||
(currentOrg) =>
|
||||
currentOrg.orgId === newOrg.orgId &&
|
||||
currentOrg.roleId === newOrg.roleId
|
||||
)
|
||||
);
|
||||
|
||||
if (orgsToAdd.length > 0) {
|
||||
await trx.insert(userOrgs).values(
|
||||
orgsToAdd.map((org) => ({
|
||||
userId: userId!,
|
||||
orgId: org.orgId,
|
||||
roleId: org.roleId,
|
||||
dateCreated: new Date().toISOString()
|
||||
roleId: org.roleId
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const token = generateSessionToken();
|
||||
const sess = await createSession(token, existingUserId!);
|
||||
const sess = await createSession(token, userId!);
|
||||
const isSecure = req.protocol === "https";
|
||||
const cookie = serializeSessionCookie(
|
||||
token,
|
||||
|
@ -225,9 +216,9 @@ export async function oidcAutoProvision({
|
|||
function hydrateOrgMapping(
|
||||
orgMapping: string | null,
|
||||
orgId: string
|
||||
): string | undefined {
|
||||
): string | null {
|
||||
if (!orgMapping) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
return orgMapping.split("{{orgId}}").join(orgId);
|
||||
return orgMapping.replaceAll("{{orgId}}", orgId);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
|
@ -12,8 +7,8 @@ import createHttpError from "http-errors";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { idp, idpOrg } from "@server/db/schemas";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
|
@ -64,6 +59,7 @@ export async function updateIdpOrgPolicy(
|
|||
)
|
||||
);
|
||||
}
|
||||
const { idpId, orgId } = parsedParams.data;
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
|
@ -74,11 +70,9 @@ export async function updateIdpOrgPolicy(
|
|||
)
|
||||
);
|
||||
}
|
||||
let { orgMapping, roleMapping } = parsedBody.data;
|
||||
|
||||
const { idpId, orgId } = parsedParams.data;
|
||||
const { roleMapping, orgMapping } = parsedBody.data;
|
||||
|
||||
// Check if IDP and policy exist
|
||||
// Given identity provider must exist and have a policy already
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
|
@ -90,36 +84,36 @@ export async function updateIdpOrgPolicy(
|
|||
|
||||
if (!existing?.idp) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"An IDP with this ID does not exist."
|
||||
)
|
||||
createHttpError(HttpCode.NOT_FOUND, "Idp does not exist")
|
||||
);
|
||||
}
|
||||
|
||||
if (!existing.idpOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"A policy for this IDP and org does not exist."
|
||||
HttpCode.NOT_FOUND,
|
||||
"Org policy does not exist for this idp"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update the policy
|
||||
await db
|
||||
.update(idpOrg)
|
||||
.set({
|
||||
roleMapping,
|
||||
orgMapping
|
||||
idpId,
|
||||
orgId,
|
||||
orgMapping,
|
||||
roleMapping
|
||||
})
|
||||
.where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId)));
|
||||
.where(and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)));
|
||||
|
||||
return response<UpdateIdpOrgPolicyResponse>(res, {
|
||||
data: {},
|
||||
data: {
|
||||
idpId
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Policy updated successfully",
|
||||
message: "Idp org policy updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -11,7 +11,6 @@ import { idp, idpOidcConfig } from "@server/db/schemas";
|
|||
import { eq } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import license from "@server/license/license";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
|
@ -100,10 +99,6 @@ export async function updateOidcIdp(
|
|||
defaultOrgMapping
|
||||
} = parsedBody.data;
|
||||
|
||||
if (!(await license.isUnlocked())) {
|
||||
autoProvision = false;
|
||||
}
|
||||
|
||||
// Check if IDP exists and is of type OIDC
|
||||
const [existingIdp] = await db
|
||||
.select()
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
} from "@server/auth/sessions/app";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import { oidcAutoProvision } from "./oidcAutoProvision";
|
||||
import license from "@server/license/license";
|
||||
|
||||
const ensureTrailingSlash = (url: string): string => {
|
||||
return url;
|
||||
|
@ -210,14 +209,6 @@ export async function validateOidcCallback(
|
|||
);
|
||||
|
||||
if (existingIdp.idp.autoProvision) {
|
||||
if (!(await license.isUnlocked())) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Auto-provisioning is not available"
|
||||
)
|
||||
);
|
||||
}
|
||||
await oidcAutoProvision({
|
||||
idp: existingIdp.idp,
|
||||
userIdentifier,
|
||||
|
|
|
@ -1,499 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import * as site from "./site";
|
||||
import * as org from "./org";
|
||||
import * as resource from "./resource";
|
||||
import * as domain from "./domain";
|
||||
import * as target from "./target";
|
||||
import * as user from "./user";
|
||||
import * as role from "./role";
|
||||
// import * as client from "./client";
|
||||
import * as accessToken from "./accessToken";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
import * as idp from "./idp";
|
||||
import {
|
||||
verifyApiKey,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction,
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyApiKeyAccessTokenAccess,
|
||||
verifyApiKeyIsRoot
|
||||
} from "@server/middlewares";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Router } from "express";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
|
||||
export const unauthenticated = Router();
|
||||
|
||||
unauthenticated.get("/", (_, res) => {
|
||||
res.status(HttpCode.OK).json({ message: "Healthy" });
|
||||
});
|
||||
|
||||
export const authenticated = Router();
|
||||
authenticated.use(verifyApiKey);
|
||||
|
||||
authenticated.get(
|
||||
"/org/checkId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.checkOrgId),
|
||||
org.checkId
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrg),
|
||||
org.createOrg
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/orgs",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.listOrgs),
|
||||
org.listOrgs
|
||||
); // TODO we need to check the orgs here
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getOrg),
|
||||
org.getOrg
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrg),
|
||||
org.updateOrg
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteOrg),
|
||||
org.deleteOrg
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
||||
site.createSite
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/sites",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listSites),
|
||||
site.listSites
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/site/:niceId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getSite),
|
||||
site.getSite
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/pick-site-defaults",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
||||
site.pickSiteDefaults
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site/:siteId",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getSite),
|
||||
site.getSite
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site/:siteId",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSite),
|
||||
site.updateSite
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/site/:siteId",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteSite),
|
||||
site.deleteSite
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site/:siteId/resources",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||
resource.listResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/resources",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||
resource.listResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domains",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listOrgDomains),
|
||||
domain.listDomains
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/create-invite",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.inviteUser),
|
||||
user.inviteUser
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/roles",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
|
||||
resource.listResourceRoles
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/users",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
|
||||
resource.listResourceUsers
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResource),
|
||||
resource.getResource
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
||||
resource.updateResource
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResource),
|
||||
resource.deleteResource
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/target",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createTarget),
|
||||
target.createTarget
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/targets",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listTargets),
|
||||
target.listTargets
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/rules",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceRules),
|
||||
resource.listResourceRules
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getTarget),
|
||||
target.getTarget
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateTarget),
|
||||
target.updateTarget
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteTarget),
|
||||
target.deleteTarget
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/role",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createRole),
|
||||
role.createRole
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/roles",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listRoles),
|
||||
role.listRoles
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/role/:roleId",
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteRole),
|
||||
role.deleteRole
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/role/:roleId/add/:userId",
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
||||
user.addUserRole
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/roles",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/users",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist),
|
||||
resource.getResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/transfer`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
||||
resource.transferResource
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/access-token/:accessTokenId`,
|
||||
verifyApiKeyAccessTokenAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/access-tokens`,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
|
||||
accessToken.listAccessTokens
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/access-tokens`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
|
||||
accessToken.listAccessTokens
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/user/:userId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getOrgUser),
|
||||
user.getOrgUser
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/users",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listUsers),
|
||||
user.listUsers
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/user/:userId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.removeUser),
|
||||
user.removeUserOrg
|
||||
);
|
||||
|
||||
// authenticated.put(
|
||||
// "/newt",
|
||||
// verifyApiKeyHasAction(ActionsEnum.createNewt),
|
||||
// newt.createNewt
|
||||
// );
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-keys`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.listApiKeys),
|
||||
apiKeys.listOrgApiKeys
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.listApiKeyActions),
|
||||
apiKeys.listApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
`/org/:orgId/api-key`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/org/:orgId/api-key/:apiKeyId`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteApiKey),
|
||||
apiKeys.deleteApiKey
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/oidc",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
||||
idp.updateOidcIdp
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
|
||||
idp.deleteIdp
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/idp",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.listIdps),
|
||||
idp.listIdps
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/idp/:idpId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.getIdp),
|
||||
idp.getIdp
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
|
||||
idp.createIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
|
||||
idp.updateIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg),
|
||||
idp.deleteIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/idp/:idpId/org",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.listIdpOrgs),
|
||||
idp.listIdpOrgPolicies
|
||||
);
|
|
@ -4,8 +4,6 @@ import * as traefik from "@server/routers/traefik";
|
|||
import * as resource from "./resource";
|
||||
import * as badger from "./badger";
|
||||
import * as auth from "@server/routers/auth";
|
||||
import * as supporterKey from "@server/routers/supporterKey";
|
||||
import * as license from "@server/routers/license";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import {
|
||||
verifyResourceAccess,
|
||||
|
@ -33,16 +31,6 @@ internalRouter.post(
|
|||
resource.getExchangeToken
|
||||
);
|
||||
|
||||
internalRouter.get(
|
||||
`/supporter-key/visible`,
|
||||
supporterKey.isSupporterKeyVisible
|
||||
);
|
||||
|
||||
internalRouter.get(
|
||||
`/license/status`,
|
||||
license.getLicenseStatus
|
||||
);
|
||||
|
||||
// Gerbil routes
|
||||
const gerbilRouter = Router();
|
||||
internalRouter.use("/gerbil", gerbilRouter);
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import license, { LicenseStatus } from "@server/license/license";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
licenseKey: z.string().min(1).max(255)
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ActivateLicenseStatus = LicenseStatus;
|
||||
|
||||
export async function activateLicense(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { licenseKey } = parsedBody.data;
|
||||
|
||||
try {
|
||||
const status = await license.activateLicenseKey(licenseKey);
|
||||
return sendResponse(res, {
|
||||
data: status,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "License key activated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { licenseKey } from "@server/db/schemas";
|
||||
import license, { LicenseStatus } from "@server/license/license";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
licenseKey: z.string().min(1).max(255)
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type DeleteLicenseKeyResponse = LicenseStatus;
|
||||
|
||||
export async function deleteLicenseKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { licenseKey: key } = parsedParams.data;
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(licenseKey)
|
||||
.where(eq(licenseKey.licenseKeyId, key))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`License key ${key} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.delete(licenseKey).where(eq(licenseKey.licenseKeyId, key));
|
||||
|
||||
const status = await license.forceRecheck();
|
||||
|
||||
return sendResponse(res, {
|
||||
data: status,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "License key deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import license, { LicenseStatus } from "@server/license/license";
|
||||
|
||||
export type GetLicenseStatusResponse = LicenseStatus;
|
||||
|
||||
export async function getLicenseStatus(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const status = await license.check();
|
||||
|
||||
return sendResponse<GetLicenseStatusResponse>(res, {
|
||||
data: status,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Got status",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
export * from "./getLicenseStatus";
|
||||
export * from "./activateLicense";
|
||||
export * from "./listLicenseKeys";
|
||||
export * from "./deleteLicenseKey";
|
||||
export * from "./recheckStatus";
|
|
@ -1,36 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import license, { LicenseKeyCache } from "@server/license/license";
|
||||
|
||||
export type ListLicenseKeysResponse = LicenseKeyCache[];
|
||||
|
||||
export async function listLicenseKeys(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const keys = license.listKeys();
|
||||
|
||||
return sendResponse<ListLicenseKeysResponse>(res, {
|
||||
data: keys,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Successfully retrieved license keys",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import license, { LicenseStatus } from "@server/license/license";
|
||||
|
||||
export type RecheckStatusResponse = LicenseStatus;
|
||||
|
||||
export async function recheckStatus(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
try {
|
||||
const status = await license.forceRecheck();
|
||||
return sendResponse(res, {
|
||||
data: status,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "License status rechecked successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ export async function createNewt(
|
|||
|
||||
const { newtId, secret } = parsedBody.data;
|
||||
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
if (req.user && !req.userRoleIds) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
|
|
@ -11,12 +11,13 @@ import {
|
|||
users,
|
||||
userSites
|
||||
} from "@server/db/schemas";
|
||||
import { and, count, eq, inArray } from "drizzle-orm";
|
||||
import { and, count, eq, inArray, countDistinct } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { RoleItem } from "../user/getOrgUser";
|
||||
|
||||
const getOrgParamsSchema = z
|
||||
.object({
|
||||
|
@ -27,7 +28,7 @@ const getOrgParamsSchema = z
|
|||
export type GetOrgOverviewResponse = {
|
||||
orgName: string;
|
||||
orgId: string;
|
||||
userRoleName: string;
|
||||
roles: RoleItem[];
|
||||
numSites: number;
|
||||
numUsers: number;
|
||||
numResources: number;
|
||||
|
@ -115,24 +116,25 @@ export async function getOrgOverview(
|
|||
);
|
||||
|
||||
const [{ numUsers }] = await db
|
||||
.select({ numUsers: count() })
|
||||
.select({ numUsers: countDistinct(userOrgs.userId) })
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.orgId, orgId));
|
||||
|
||||
const [role] = await db
|
||||
.select()
|
||||
const userRoles = await db
|
||||
.select({ id: roles.roleId, name: roles.name })
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, req.userOrg.roleId));
|
||||
.where(inArray(roles.roleId, req.userRoleIds ?? []))
|
||||
.orderBy(roles.name);
|
||||
|
||||
return response<GetOrgOverviewResponse>(res, {
|
||||
data: {
|
||||
orgName: org[0].name,
|
||||
orgId: org[0].orgId,
|
||||
userRoleName: role.name,
|
||||
roles: userRoles,
|
||||
numSites,
|
||||
numUsers,
|
||||
numResources,
|
||||
isAdmin: role.name === "Admin",
|
||||
isAdmin: userRoles.some((r) => r.name === "Admin"),
|
||||
isOwner: req.userOrg?.isOwner || false
|
||||
},
|
||||
success: true,
|
||||
|
|
|
@ -130,7 +130,7 @@ export async function createResource(
|
|||
|
||||
const { siteId, orgId } = parsedParams.data;
|
||||
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
if (req.user && !req.userRoleIds) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
@ -285,7 +285,7 @@ async function createHttpResource(
|
|||
resourceId: newResource[0].resourceId
|
||||
});
|
||||
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) {
|
||||
// make sure the user can access the resource
|
||||
await trx.insert(userResources).values({
|
||||
userId: req.user?.userId!,
|
||||
|
@ -392,7 +392,7 @@ async function createRawResource(
|
|||
resourceId: newResource[0].resourceId
|
||||
});
|
||||
|
||||
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) {
|
||||
// make sure the user can access the resource
|
||||
await trx.insert(userResources).values({
|
||||
userId: req.user?.userId!,
|
||||
|
|
|
@ -216,7 +216,7 @@ export async function listResources(
|
|||
.where(
|
||||
or(
|
||||
eq(userResources.userId, req.user!.userId),
|
||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
inArray(roleResources.roleId, req.userRoleIds!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -99,7 +99,7 @@ export async function createSite(
|
|||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
if (req.user && !req.userRoleIds) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
@ -176,7 +176,7 @@ export async function createSite(
|
|||
siteId: newSite.siteId
|
||||
});
|
||||
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) {
|
||||
// make sure the user can access the site
|
||||
trx.insert(userSites).values({
|
||||
userId: req.user?.userId!,
|
||||
|
|
|
@ -120,7 +120,7 @@ export async function listSites(
|
|||
.where(
|
||||
or(
|
||||
eq(userSites.userId, req.user!.userId),
|
||||
eq(roleSites.roleId, req.userOrgRoleId!)
|
||||
inArray(roleSites.roleId, req.userRoleIds!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export type HideSupporterKeyResponse = {
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
export async function hideSupporterKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
config.hideSupporterKey();
|
||||
|
||||
return sendResponse<HideSupporterKeyResponse>(res, {
|
||||
data: {
|
||||
hidden: true
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Hidden",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./validateSupporterKey";
|
||||
export * from "./isSupporterKeyVisible";
|
||||
export * from "./hideSupporterKey";
|
|
@ -1,63 +0,0 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import config from "@server/lib/config";
|
||||
import db from "@server/db";
|
||||
import { count } from "drizzle-orm";
|
||||
import { users } from "@server/db/schemas";
|
||||
import license from "@server/license/license";
|
||||
|
||||
export type IsSupporterKeyVisibleResponse = {
|
||||
visible: boolean;
|
||||
tier?: string;
|
||||
};
|
||||
|
||||
const USER_LIMIT = 5;
|
||||
|
||||
export async function isSupporterKeyVisible(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const hidden = config.isSupporterKeyHidden();
|
||||
const key = config.getSupporterData();
|
||||
|
||||
let visible = !hidden && key?.valid !== true;
|
||||
|
||||
const licenseStatus = await license.check();
|
||||
|
||||
if (licenseStatus.isLicenseValid) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
if (key?.tier === "Limited Supporter") {
|
||||
const [numUsers] = await db.select({ count: count() }).from(users);
|
||||
|
||||
if (numUsers.count > USER_LIMIT) {
|
||||
logger.debug(
|
||||
`User count ${numUsers.count} exceeds limit ${USER_LIMIT}`
|
||||
);
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
return sendResponse<IsSupporterKeyVisibleResponse>(res, {
|
||||
data: {
|
||||
visible,
|
||||
tier: key?.tier || undefined
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Status",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { response as sendResponse } from "@server/lib";
|
||||
import { suppressDeprecationWarnings } from "moment";
|
||||
import { supporterKey } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const validateSupporterKeySchema = z
|
||||
.object({
|
||||
githubUsername: z.string().nonempty(),
|
||||
key: z.string().nonempty()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ValidateSupporterKeyResponse = {
|
||||
valid: boolean;
|
||||
githubUsername?: string;
|
||||
tier?: string;
|
||||
phrase?: string;
|
||||
};
|
||||
|
||||
export async function validateSupporterKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = validateSupporterKeySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { githubUsername, key } = parsedBody.data;
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.fossorial.io/api/v1/license/validate",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
licenseKey: key,
|
||||
githubUsername: githubUsername
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error(response);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.data.valid) {
|
||||
return sendResponse<ValidateSupporterKeyResponse>(res, {
|
||||
data: {
|
||||
valid: false
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Invalid supporter key",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.delete(supporterKey);
|
||||
await trx.insert(supporterKey).values({
|
||||
githubUsername: githubUsername,
|
||||
key: key,
|
||||
tier: data.data.tier || null,
|
||||
phrase: data.data.cutePhrase || null,
|
||||
valid: true
|
||||
});
|
||||
});
|
||||
|
||||
await config.checkSupporterKey();
|
||||
|
||||
return sendResponse<ValidateSupporterKeyResponse>(res, {
|
||||
data: {
|
||||
valid: true,
|
||||
githubUsername: data.data.githubUsername,
|
||||
tier: data.data.tier,
|
||||
phrase: data.data.cutePhrase
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Valid supporter key",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -105,14 +105,26 @@ export async function addUserRole(
|
|||
);
|
||||
}
|
||||
|
||||
const newUserRole = await db
|
||||
.update(userOrgs)
|
||||
.set({ roleId })
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.returning();
|
||||
const newUserRole = { orgId, userId, roleId, isOwner: false };
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const hasRoleAlready = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, orgId),
|
||||
eq(userOrgs.roleId, roleId)
|
||||
)
|
||||
);
|
||||
if (hasRoleAlready.length === 0) {
|
||||
await trx.insert(userOrgs).values(newUserRole);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: newUserRole[0],
|
||||
data: newUserRole,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Role added to user successfully",
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
|
|||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userOrgs, users } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
@ -10,6 +10,7 @@ import logger from "@server/logger";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { StringDecoder } from "string_decoder";
|
||||
|
||||
async function queryUser(orgId: string, userId: string) {
|
||||
const [user] = await db
|
||||
|
@ -20,8 +21,7 @@ async function queryUser(orgId: string, userId: string) {
|
|||
username: users.username,
|
||||
name: users.name,
|
||||
type: users.type,
|
||||
roleId: userOrgs.roleId,
|
||||
roleName: roles.name,
|
||||
roles: sql<RoleItem[]>`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`,
|
||||
isOwner: userOrgs.isOwner,
|
||||
isAdmin: roles.isAdmin
|
||||
})
|
||||
|
@ -30,9 +30,17 @@ async function queryUser(orgId: string, userId: string) {
|
|||
.leftJoin(users, eq(userOrgs.userId, users.userId))
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
if (typeof user.roles === "string") {
|
||||
user.roles = JSON.parse(user.roles);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
export type RoleItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type GetOrgUserResponse = NonNullable<
|
||||
Awaited<ReturnType<typeof queryUser>>
|
||||
>;
|
||||
|
|
|
@ -2,6 +2,7 @@ export * from "./getUser";
|
|||
export * from "./removeUserOrg";
|
||||
export * from "./listUsers";
|
||||
export * from "./addUserRole";
|
||||
export * from "./setUserRoles";
|
||||
export * from "./inviteUser";
|
||||
export * from "./acceptInvite";
|
||||
export * from "./getOrgUser";
|
||||
|
|
|
@ -5,11 +5,11 @@ import { idp, roles, userOrgs, users } from "@server/db/schemas";
|
|||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { and, sql } from "drizzle-orm";
|
||||
import { AnyColumn, eq, InferColumnsDataTypes, sql } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { RoleItem } from "./getOrgUser";
|
||||
|
||||
const listUsersParamsSchema = z
|
||||
.object({
|
||||
|
@ -34,8 +34,20 @@ const listUsersSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
function jsonAggBuildObject<T extends Record<string, AnyColumn>>(shape: T) {
|
||||
const shapeString = Object.entries(shape)
|
||||
.map(([key, value]) => {
|
||||
return `'${key}', ${value}`;
|
||||
})
|
||||
.join(",");
|
||||
|
||||
return sql<
|
||||
InferColumnsDataTypes<T>[]
|
||||
>`json_agg(json_build_object(${shapeString}))`;
|
||||
}
|
||||
|
||||
async function queryUsers(orgId: string, limit: number, offset: number) {
|
||||
return await db
|
||||
const res = await db
|
||||
.select({
|
||||
id: users.userId,
|
||||
email: users.email,
|
||||
|
@ -45,8 +57,7 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
|
|||
username: users.username,
|
||||
name: users.name,
|
||||
type: users.type,
|
||||
roleId: userOrgs.roleId,
|
||||
roleName: roles.name,
|
||||
roles: sql<RoleItem[]>`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`,
|
||||
isOwner: userOrgs.isOwner,
|
||||
idpName: idp.name,
|
||||
idpId: users.idpId
|
||||
|
@ -56,8 +67,15 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
|
|||
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||
.where(eq(userOrgs.orgId, orgId))
|
||||
.groupBy(users.userId)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
res.forEach((itm) => {
|
||||
if (typeof itm.roles === "string") {
|
||||
itm.roles = JSON.parse(itm.roles);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export type ListUsersResponse = {
|
||||
|
|
175
server/routers/user/setUserRoles.ts
Normal file
175
server/routers/user/setUserRoles.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs, roles } from "@server/db/schemas";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setUserRolesParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string(),
|
||||
userId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const setUserRolesBodySchema = z.object({
|
||||
roleIds: z.array(z.number().int()).min(1)
|
||||
});
|
||||
|
||||
export type SetUserRolesResponse = z.infer<typeof setUserRolesBodySchema>;
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/user/{userId}/roles",
|
||||
description: "Set the roles of an user",
|
||||
tags: [OpenAPITags.Role, OpenAPITags.User],
|
||||
request: {
|
||||
params: setUserRolesParamsSchema,
|
||||
body: {
|
||||
content: { "application/json": { schema: setUserRolesBodySchema } }
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setUserRoles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setUserRolesParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { userId, orgId } = parsedParams.data;
|
||||
|
||||
const parsedBody = setUserRolesBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
let { roleIds: newRoles } = parsedBody.data;
|
||||
newRoles = [...new Set(newRoles)];
|
||||
newRoles.sort((a, b) => a - b);
|
||||
if (newRoles.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"You need to set at least 1 role"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ((req.userOrg?.orgId || req.apiKeyOrg?.orgId) !== orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You do not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const existingRoles = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.orderBy(userOrgs.roleId);
|
||||
|
||||
if (existingRoles.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"User not found or does not belong to the specified organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingRoles[0].isOwner) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the role of the owner of the organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
existingRoles.length === newRoles.length &&
|
||||
existingRoles.every((r, i) => r.roleId === newRoles[i])
|
||||
) {
|
||||
return response(res, {
|
||||
data: { roles: newRoles },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User roles unchanged",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
const rolesToCheck = newRoles.filter(
|
||||
(r) => !existingRoles.some((er) => er.roleId === r)
|
||||
);
|
||||
if (rolesToCheck.length > 0) {
|
||||
const roleChkRes = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.orgId, orgId),
|
||||
inArray(roles.roleId, rolesToCheck)
|
||||
)
|
||||
);
|
||||
if (roleChkRes.length !== rolesToCheck.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Role not found or does not belong to the specified organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(userOrgs)
|
||||
.where(
|
||||
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||
);
|
||||
const newValues = newRoles.map((roleId) => ({
|
||||
userId,
|
||||
orgId,
|
||||
roleId,
|
||||
isOwner: false
|
||||
}));
|
||||
await trx.insert(userOrgs).values(newValues);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: { roles: newRoles },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User roles set successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,8 @@ import {
|
|||
CardFooter
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react";
|
||||
import { Users, Settings, Waypoints, Combine } from "lucide-react";
|
||||
import { RoleItem } from "@server/routers/user";
|
||||
|
||||
interface OrgStat {
|
||||
label: string;
|
||||
|
@ -26,7 +27,7 @@ type OrganizationLandingCardProps = {
|
|||
resources: number;
|
||||
users: number;
|
||||
};
|
||||
userRole: string;
|
||||
roles: RoleItem[];
|
||||
isAdmin: boolean;
|
||||
isOwner: boolean;
|
||||
orgId: string;
|
||||
|
@ -81,9 +82,21 @@ export default function OrganizationLandingCard(
|
|||
))}
|
||||
</div>
|
||||
<div className="text-center text-lg">
|
||||
Your role:{" "}
|
||||
Your role
|
||||
{orgData.overview.isOwner ||
|
||||
orgData.overview.isAdmin ||
|
||||
orgData.overview.roles.length === 1
|
||||
? ""
|
||||
: "s"}
|
||||
:{" "}
|
||||
<span className="font-semibold">
|
||||
{orgData.overview.isOwner ? "Owner" : orgData.overview.userRole}
|
||||
{orgData.overview.isOwner
|
||||
? "Owner"
|
||||
: orgData.overview.isAdmin
|
||||
? "Admin"
|
||||
: orgData.overview.roles
|
||||
.map((r) => r.name)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
@ -74,7 +74,7 @@ export default async function OrgPage(props: OrgPageProps) {
|
|||
},
|
||||
isAdmin: overview.isAdmin,
|
||||
isOwner: overview.isOwner,
|
||||
userRole: overview.userRoleName
|
||||
roles: overview.roles
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -150,7 +150,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Role
|
||||
Roles
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -8,17 +8,9 @@ import {
|
|||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { InviteUserResponse } from "@server/routers/user";
|
||||
import { SetUserRolesResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
@ -40,10 +32,18 @@ import {
|
|||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email({ message: "Please enter a valid email" }),
|
||||
roleId: z.string().min(1, { message: "Please select a role" })
|
||||
roles: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string()
|
||||
})
|
||||
)
|
||||
.min(1, { message: "Please select a role" })
|
||||
});
|
||||
|
||||
export default function AccessControlsPage() {
|
||||
|
@ -54,13 +54,18 @@ export default function AccessControlsPage() {
|
|||
const { orgId } = useParams();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(
|
||||
[]
|
||||
);
|
||||
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: user.email!,
|
||||
roleId: user.roleId?.toString()
|
||||
roles: []
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -81,13 +86,24 @@ export default function AccessControlsPage() {
|
|||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
setRoles(res.data.data.roles);
|
||||
setAllRoles(
|
||||
res.data.data.roles.map((role) => ({
|
||||
id: role.roleId.toString(),
|
||||
text: role.name
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fetchRoles();
|
||||
|
||||
form.setValue("roleId", user.roleId.toString());
|
||||
form.setValue(
|
||||
"roles",
|
||||
user.roles.map((i) => ({
|
||||
id: i.id.toString(),
|
||||
text: i.name
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
|
@ -95,8 +111,8 @@ export default function AccessControlsPage() {
|
|||
|
||||
const res = await api
|
||||
.post<
|
||||
AxiosResponse<InviteUserResponse>
|
||||
>(`/role/${values.roleId}/add/${user.userId}`)
|
||||
AxiosResponse<SetUserRolesResponse>
|
||||
>(`/org/${user.orgId}/user/${user.userId}/roles`, { roleIds: values.roles.map((r) => parseInt(r.id)) })
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
|
@ -140,30 +156,44 @@ export default function AccessControlsPage() {
|
|||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roleId"
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem
|
||||
key={role.roleId}
|
||||
value={role.roleId.toString()}
|
||||
>
|
||||
{role.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeRolesTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder="Select a role"
|
||||
size="sm"
|
||||
tags={
|
||||
form.getValues().roles
|
||||
}
|
||||
setTags={(newRoles) => {
|
||||
form.setValue(
|
||||
"roles",
|
||||
newRoles as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={true}
|
||||
autocompleteOptions={
|
||||
allRoles
|
||||
}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
|
@ -78,7 +78,7 @@ export default async function UsersPage(props: UsersPageProps) {
|
|||
idpId: user.idpId,
|
||||
idpName: user.idpName || "Internal",
|
||||
status: "Confirmed",
|
||||
role: user.isOwner ? "Owner" : user.roleName || "Member",
|
||||
role: user.isOwner ? "Owner" : user.roles.map((r) => r.name).join(", ") || "Member",
|
||||
isOwner: user.isOwner || false
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
addApiKey?: () => void;
|
||||
}
|
||||
|
||||
export function OrgApiKeysDataTable<TData, TValue>({
|
||||
addApiKey,
|
||||
columns,
|
||||
data
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="API Keys"
|
||||
searchPlaceholder="Search API keys..."
|
||||
searchColumn="name"
|
||||
onAdd={addApiKey}
|
||||
addButtonText="Generate API Key"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { OrgApiKeysDataTable } from "./OrgApiKeysDataTable";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import moment from "moment";
|
||||
|
||||
export type OrgApiKeyRow = {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type OrgApiKeyTableProps = {
|
||||
apiKeys: OrgApiKeyRow[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function OrgApiKeysTable({
|
||||
apiKeys,
|
||||
orgId
|
||||
}: OrgApiKeyTableProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<OrgApiKeyRow | null>(null);
|
||||
const [rows, setRows] = useState<OrgApiKeyRow[]>(apiKeys);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const deleteSite = (apiKeyId: string) => {
|
||||
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
|
||||
.catch((e) => {
|
||||
console.error("Error deleting API key", e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error deleting API key",
|
||||
description: formatAxiosError(e, "Error deleting API key")
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
router.refresh();
|
||||
setIsDeleteModalOpen(false);
|
||||
|
||||
const newRows = rows.filter((row) => row.id !== apiKeyId);
|
||||
|
||||
setRows(newRows);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnDef<OrgApiKeyRow>[] = [
|
||||
{
|
||||
id: "dots",
|
||||
cell: ({ row }) => {
|
||||
const apiKeyROw = row.original;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelected(apiKeyROw);
|
||||
}}
|
||||
>
|
||||
<span>View settings</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelected(apiKeyROw);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Name
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Key",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
||||
<Button variant={"outlinePrimary"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selected && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to remove the API key{" "}
|
||||
<b>{selected?.name || selected?.id}</b> from the
|
||||
organization?
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>
|
||||
Once removed, the API key will no longer be
|
||||
able to be used.
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To confirm, please type the name of the API key
|
||||
below.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Delete API Key"
|
||||
onConfirm={async () => deleteSite(selected!.id)}
|
||||
string={selected.name}
|
||||
title="Delete API Key"
|
||||
/>
|
||||
)}
|
||||
|
||||
<OrgApiKeysDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
addApiKey={() => {
|
||||
router.push(`/${orgId}/settings/api-keys/create`);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { internal } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
||||
import Link from "next/link";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from "@app/components/ui/breadcrumb";
|
||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ apiKeyId: string; orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
let apiKey = null;
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetApiKeyResponse>>(
|
||||
`/org/${params.orgId}/api-key/${params.apiKeyId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
apiKey = res.data.data;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
redirect(`/${params.orgId}/settings/api-keys`);
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "Permissions",
|
||||
href: "/{orgId}/settings/api-keys/{apiKeyId}/permissions"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle title={`${apiKey?.name} Settings`} />
|
||||
|
||||
<ApiKeyProvider apiKey={apiKey}>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</ApiKeyProvider>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ApiKeysPage(props: {
|
||||
params: Promise<{ orgId: string; apiKeyId: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`);
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { ListApiKeyActionsResponse } from "@server/routers/apiKeys";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId, apiKeyId } = useParams();
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState<boolean>(true);
|
||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [loadingSavePermissions, setLoadingSavePermissions] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
setLoadingPage(true);
|
||||
|
||||
const res = await api
|
||||
.get<
|
||||
AxiosResponse<ListApiKeyActionsResponse>
|
||||
>(`/org/${orgId}/api-key/${apiKeyId}/actions`)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error loading API key actions",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"Error loading API key actions"
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
for (const action of data.actions) {
|
||||
setSelectedPermissions((prev) => ({
|
||||
...prev,
|
||||
[action.actionId]: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingPage(false);
|
||||
}
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function savePermissions() {
|
||||
setLoadingSavePermissions(true);
|
||||
|
||||
const actionsRes = await api
|
||||
.post(`/org/${orgId}/api-key/${apiKeyId}/actions`, {
|
||||
actionIds: Object.keys(selectedPermissions).filter(
|
||||
(key) => selectedPermissions[key]
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Error setting permissions", e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error setting permissions",
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (actionsRes && actionsRes.status === 200) {
|
||||
toast({
|
||||
title: "Permissions updated",
|
||||
description: "The permissions have been updated."
|
||||
});
|
||||
}
|
||||
|
||||
setLoadingSavePermissions(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loadingPage && (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Permissions
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Determine what this API key can do
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<PermissionsSelectBox
|
||||
selectedPermissions={selectedPermissions}
|
||||
onChange={setSelectedPermissions}
|
||||
/>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await savePermissions();
|
||||
}}
|
||||
loading={loadingSavePermissions}
|
||||
disabled={loadingSavePermissions}
|
||||
>
|
||||
Save Permissions
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,412 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { z } from "zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from "@app/components/ui/breadcrumb";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
CreateOrgApiKeyBody,
|
||||
CreateOrgApiKeyResponse
|
||||
} from "@server/routers/apiKeys";
|
||||
import { ApiKey } from "@server/db/schemas";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import moment from "moment";
|
||||
import CopyCodeBox from "@server/emails/templates/components/CopyCodeBox";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
||||
|
||||
const createFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Name must be at least 2 characters."
|
||||
})
|
||||
.max(255, {
|
||||
message: "Name must not be longer than 255 characters."
|
||||
})
|
||||
});
|
||||
|
||||
type CreateFormValues = z.infer<typeof createFormSchema>;
|
||||
|
||||
const copiedFormSchema = z
|
||||
.object({
|
||||
copied: z.boolean()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return data.copied;
|
||||
},
|
||||
{
|
||||
message: "You must confirm that you have copied the API key.",
|
||||
path: ["copied"]
|
||||
}
|
||||
);
|
||||
|
||||
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
|
||||
|
||||
export default function Page() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [apiKey, setApiKey] = useState<CreateOrgApiKeyResponse | null>(null);
|
||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const form = useForm<CreateFormValues>({
|
||||
resolver: zodResolver(createFormSchema),
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
});
|
||||
|
||||
const copiedForm = useForm<CopiedFormValues>({
|
||||
resolver: zodResolver(copiedFormSchema),
|
||||
defaultValues: {
|
||||
copied: false
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit(data: CreateFormValues) {
|
||||
setCreateLoading(true);
|
||||
|
||||
let payload: CreateOrgApiKeyBody = {
|
||||
name: data.name
|
||||
};
|
||||
|
||||
const res = await api
|
||||
.put<
|
||||
AxiosResponse<CreateOrgApiKeyResponse>
|
||||
>(`/org/${orgId}/api-key/`, payload)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error creating API key",
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 201) {
|
||||
const data = res.data.data;
|
||||
|
||||
console.log({
|
||||
actionIds: Object.keys(selectedPermissions).filter(
|
||||
(key) => selectedPermissions[key]
|
||||
)
|
||||
});
|
||||
|
||||
const actionsRes = await api
|
||||
.post(`/org/${orgId}/api-key/${data.apiKeyId}/actions`, {
|
||||
actionIds: Object.keys(selectedPermissions).filter(
|
||||
(key) => selectedPermissions[key]
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Error setting permissions", e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error setting permissions",
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (actionsRes) {
|
||||
setApiKey(data);
|
||||
}
|
||||
}
|
||||
|
||||
setCreateLoading(false);
|
||||
}
|
||||
|
||||
async function onCopiedSubmit(data: CopiedFormValues) {
|
||||
if (!data.copied) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}
|
||||
|
||||
const formatLabel = (str: string) => {
|
||||
return str
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/^./, (char) => char.toUpperCase());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoadingPage(false);
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title="Generate API Key"
|
||||
description="Generate a new API key for your organization"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}}
|
||||
>
|
||||
See All API Keys
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!loadingPage && (
|
||||
<div>
|
||||
<SettingsContainer>
|
||||
{!apiKey && (
|
||||
<>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
API Key Information
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-site-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Permissions
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Determine what this API key can do
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<PermissionsSelectBox
|
||||
selectedPermissions={
|
||||
selectedPermissions
|
||||
}
|
||||
onChange={setSelectedPermissions}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</>
|
||||
)}
|
||||
|
||||
{apiKey && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Your API Key
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Name
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
text={apiKey.name}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Created
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{moment(
|
||||
apiKey.createdAt
|
||||
).format("lll")}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Save Your API Key
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
You will only be able to see this
|
||||
once. Make sure to copy it to a
|
||||
secure place.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<h4 className="font-semibold">
|
||||
Your API key is:
|
||||
</h4>
|
||||
|
||||
<CopyTextBox
|
||||
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
||||
/>
|
||||
|
||||
<Form {...copiedForm}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="copied-form"
|
||||
>
|
||||
<FormField
|
||||
control={copiedForm.control}
|
||||
name="copied"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
defaultChecked={
|
||||
copiedForm.getValues(
|
||||
"copied"
|
||||
) as boolean
|
||||
}
|
||||
onCheckedChange={(
|
||||
e
|
||||
) => {
|
||||
copiedForm.setValue(
|
||||
"copied",
|
||||
e as boolean
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
I have copied
|
||||
the API key
|
||||
</label>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
{!apiKey && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={createLoading || apiKey !== null}
|
||||
onClick={() => {
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{!apiKey && (
|
||||
<Button
|
||||
type="button"
|
||||
loading={createLoading}
|
||||
disabled={createLoading || apiKey !== null}
|
||||
onClick={() => {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{apiKey && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
copiedForm.handleSubmit(onCopiedSubmit)();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import OrgApiKeysTable, { OrgApiKeyRow } from "./OrgApiKeysTable";
|
||||
import { ListOrgApiKeysResponse } from "@server/routers/apiKeys";
|
||||
|
||||
type ApiKeyPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
||||
const params = await props.params;
|
||||
let apiKeys: ListOrgApiKeysResponse["apiKeys"] = [];
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<ListOrgApiKeysResponse>>(
|
||||
`/org/${params.orgId}/api-keys`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
apiKeys = res.data.data.apiKeys;
|
||||
} catch (e) {}
|
||||
|
||||
const rows: OrgApiKeyRow[] = apiKeys.map((key) => {
|
||||
return {
|
||||
name: key.name,
|
||||
id: key.apiKeyId,
|
||||
key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`,
|
||||
createdAt: key.createdAt
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title="Manage API Keys"
|
||||
description="API keys are used to authenticate with the integration API"
|
||||
/>
|
||||
|
||||
<OrgApiKeysTable apiKeys={rows} orgId={params.orgId} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
getSortedRowModel,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
addApiKey?: () => void;
|
||||
}
|
||||
|
||||
export function ApiKeysDataTable<TData, TValue>({
|
||||
addApiKey,
|
||||
columns,
|
||||
data
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="API Keys"
|
||||
searchPlaceholder="Search API keys..."
|
||||
searchColumn="name"
|
||||
onAdd={addApiKey}
|
||||
addButtonText="Generate API Key"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import moment from "moment";
|
||||
import { ApiKeysDataTable } from "./ApiKeysDataTable";
|
||||
|
||||
export type ApiKeyRow = {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type ApiKeyTableProps = {
|
||||
apiKeys: ApiKeyRow[];
|
||||
};
|
||||
|
||||
export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<ApiKeyRow | null>(null);
|
||||
const [rows, setRows] = useState<ApiKeyRow[]>(apiKeys);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const deleteSite = (apiKeyId: string) => {
|
||||
api.delete(`/api-key/${apiKeyId}`)
|
||||
.catch((e) => {
|
||||
console.error("Error deleting API key", e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error deleting API key",
|
||||
description: formatAxiosError(e, "Error deleting API key")
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
router.refresh();
|
||||
setIsDeleteModalOpen(false);
|
||||
|
||||
const newRows = rows.filter((row) => row.id !== apiKeyId);
|
||||
|
||||
setRows(newRows);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnDef<ApiKeyRow>[] = [
|
||||
{
|
||||
id: "dots",
|
||||
cell: ({ row }) => {
|
||||
const apiKeyROw = row.original;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelected(apiKeyROw);
|
||||
}}
|
||||
>
|
||||
<span>View settings</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelected(apiKeyROw);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Name
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Key",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href={`/admin/api-keys/${r.id}`}>
|
||||
<Button variant={"outlinePrimary"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selected && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to remove the API key{" "}
|
||||
<b>{selected?.name || selected?.id}</b>?
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>
|
||||
Once removed, the API key will no longer be
|
||||
able to be used.
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To confirm, please type the name of the API key
|
||||
below.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Delete API Key"
|
||||
onConfirm={async () => deleteSite(selected!.id)}
|
||||
string={selected.name}
|
||||
title="Delete API Key"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ApiKeysDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
addApiKey={() => {
|
||||
router.push(`/admin/api-keys/create`);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { internal } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
||||
import Link from "next/link";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from "@app/components/ui/breadcrumb";
|
||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ apiKeyId: string }>;
|
||||
}
|
||||
|
||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
let apiKey = null;
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetApiKeyResponse>>(
|
||||
`/api-key/${params.apiKeyId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
apiKey = res.data.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
redirect(`/admin/api-keys`);
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "Permissions",
|
||||
href: "/admin/api-keys/{apiKeyId}/permissions"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle title={`${apiKey?.name} Settings`} />
|
||||
|
||||
<ApiKeyProvider apiKey={apiKey}>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</ApiKeyProvider>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ApiKeysPage(props: {
|
||||
params: Promise<{ apiKeyId: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(`/admin/api-keys/${params.apiKeyId}/permissions`);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue