Compare commits
	
		
			No commits in common. "7098c5e513cc692e94c964bcbb1146bc997ea970" and "21f13260456b45bd24bae3cfa7dfbe5225ad1630" have entirely different histories.
		
	
	
		
			
				7098c5e513
			
			...
			
				21f1326045
			
		
	
		
					 134 changed files with 8947 additions and 1674 deletions
				
			
		|  | @ -2,7 +2,8 @@ FROM node:20-alpine AS builder | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| 
 | 
 | ||||||
| COPY package.json package-lock.json ./ | # COPY package.json package-lock.json ./ | ||||||
|  | COPY package.json ./ | ||||||
| RUN npm install | RUN npm install | ||||||
| 
 | 
 | ||||||
| COPY . . | COPY . . | ||||||
|  | @ -20,7 +21,7 @@ RUN apk add --no-cache curl | ||||||
| 
 | 
 | ||||||
| # COPY package.json package-lock.json ./ | # COPY package.json package-lock.json ./ | ||||||
| COPY package.json ./ | COPY package.json ./ | ||||||
| RUN npm install --omit=dev && npm cache clean --force | RUN npm install --only=production && npm cache clean --force | ||||||
| 
 | 
 | ||||||
| COPY --from=builder /app/.next/standalone ./ | COPY --from=builder /app/.next/standalone ./ | ||||||
| COPY --from=builder /app/.next/static ./.next/static | COPY --from=builder /app/.next/static ./.next/static | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								LICENSE
									
										
									
									
									
								
							
							
						
						
									
										30
									
								
								LICENSE
									
										
									
									
									
								
							|  | @ -1,5 +1,35 @@ | ||||||
| Copyright (c) 2025 Fossorial, LLC. | 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 |                     GNU AFFERO GENERAL PUBLIC LICENSE | ||||||
|                        Version 3, 19 November 2007 |                        Version 3, 19 November 2007 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,9 +38,6 @@ 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._ | _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 | ## Key Features | ||||||
| 
 | 
 | ||||||
| ### Reverse Proxy Through WireGuard Tunnel | ### Reverse Proxy Through WireGuard Tunnel | ||||||
|  |  | ||||||
							
								
								
									
										529
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										529
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -383,30 +383,11 @@ | ||||||
|                 "@noble/ciphers": "^1.0.0" |                 "@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": { |         "node_modules/@emnapi/runtime": { | ||||||
|             "version": "1.4.3", |             "version": "1.3.1", | ||||||
|             "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", |             "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", | ||||||
|             "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", |             "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", | ||||||
|             "license": "MIT", |             "dev": true, | ||||||
|             "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", |             "license": "MIT", | ||||||
|             "optional": true, |             "optional": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  | @ -1637,39 +1618,6 @@ | ||||||
|                 "@jridgewell/sourcemap-codec": "^1.4.14" |                 "@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": { |         "node_modules/@next/env": { | ||||||
|             "version": "15.2.4", |             "version": "15.2.4", | ||||||
|             "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", |             "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", | ||||||
|  | @ -1880,38 +1828,6 @@ | ||||||
|                 "@node-rs/argon2-win32-x64-msvc": "2.0.2" |                 "@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": { |         "node_modules/@node-rs/argon2-darwin-arm64": { | ||||||
|             "version": "2.0.2", |             "version": "2.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", |             "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", | ||||||
|  | @ -1928,387 +1844,6 @@ | ||||||
|                 "node": ">= 10" |                 "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": { |         "node_modules/@node-rs/bcrypt": { | ||||||
|             "version": "1.9.0", |             "version": "1.9.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", |             "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", | ||||||
|  | @ -4144,16 +3679,6 @@ | ||||||
|                 "url": "https://github.com/sponsors/tannerlinsley" |                 "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": { |         "node_modules/@types/better-sqlite3": { | ||||||
|             "version": "7.6.12", |             "version": "7.6.12", | ||||||
|             "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", |             "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", | ||||||
|  | @ -7997,13 +7522,6 @@ | ||||||
|             "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", |             "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", | ||||||
|             "license": "MIT" |             "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": { |         "node_modules/fsevents": { | ||||||
|             "version": "2.3.3", |             "version": "2.3.3", | ||||||
|             "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", |             "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", | ||||||
|  | @ -9665,29 +9183,6 @@ | ||||||
|                 "node": ">= 0.6" |                 "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": { |         "node_modules/merge-descriptors": { | ||||||
|             "version": "1.0.3", |             "version": "1.0.3", | ||||||
|             "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", |             "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", | ||||||
|  | @ -12865,22 +12360,6 @@ | ||||||
|                 "node": ">= 10" |                 "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": { |         "node_modules/own-keys": { | ||||||
|             "version": "1.0.1", |             "version": "1.0.1", | ||||||
|             "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", |             "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Request } from "express"; | import { Request } from "express"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
| import { userActions, roleActions, userOrgs } from "@server/db/schemas"; | import { userActions, roleActions, userOrgs } from "@server/db/schemas"; | ||||||
| import { and, eq, inArray } from "drizzle-orm"; | import { and, eq } from "drizzle-orm"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| 
 | 
 | ||||||
|  | @ -51,7 +51,6 @@ export enum ActionsEnum { | ||||||
|     listRoleResources = "listRoleResources", |     listRoleResources = "listRoleResources", | ||||||
|     // listRoleActions = "listRoleActions",
 |     // listRoleActions = "listRoleActions",
 | ||||||
|     addUserRole = "addUserRole", |     addUserRole = "addUserRole", | ||||||
|     setUserRoles = "setUserRoles", |  | ||||||
|     // addUserSite = "addUserSite",
 |     // addUserSite = "addUserSite",
 | ||||||
|     // addUserAction = "addUserAction",
 |     // addUserAction = "addUserAction",
 | ||||||
|     // removeUserAction = "removeUserAction",
 |     // removeUserAction = "removeUserAction",
 | ||||||
|  | @ -107,28 +106,29 @@ export async function checkUserActionPermission( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         let userRoleIds = req.userRoleIds; |         let userOrgRoleId = req.userOrgRoleId; | ||||||
| 
 | 
 | ||||||
|         // If userOrgRoleId is not available on the request, fetch it
 |         // If userOrgRoleId is not available on the request, fetch it
 | ||||||
|         if (userRoleIds === undefined) { |         if (userOrgRoleId === undefined) { | ||||||
|             const userOrgRoles = await db |             const userOrgRole = await db | ||||||
|                 .select({ roleId: userOrgs.roleId }) |                 .select() | ||||||
|                 .from(userOrgs) |                 .from(userOrgs) | ||||||
|                 .where( |                 .where( | ||||||
|                     and( |                     and( | ||||||
|                         eq(userOrgs.userId, userId), |                         eq(userOrgs.userId, userId), | ||||||
|                         eq(userOrgs.orgId, req.userOrgId!) |                         eq(userOrgs.orgId, req.userOrgId!) | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ) | ||||||
|  |                 .limit(1); | ||||||
| 
 | 
 | ||||||
|             if (userOrgRoles.length === 0) { |             if (userOrgRole.length === 0) { | ||||||
|                 throw createHttpError( |                 throw createHttpError( | ||||||
|                     HttpCode.FORBIDDEN, |                     HttpCode.FORBIDDEN, | ||||||
|                     "User does not have access to this organization" |                     "User does not have access to this organization" | ||||||
|                 ); |                 ); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             userRoleIds = userOrgRoles.map((r) => r.roleId); |             userOrgRoleId = userOrgRole[0].roleId; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Check if the user has direct permission for the action in the current org
 |         // Check if the user has direct permission for the action in the current org
 | ||||||
|  | @ -155,8 +155,8 @@ export async function checkUserActionPermission( | ||||||
|             .where( |             .where( | ||||||
|                 and( |                 and( | ||||||
|                     eq(roleActions.actionId, actionId), |                     eq(roleActions.actionId, actionId), | ||||||
|                     eq(roleActions.orgId, req.userOrgId!), |                     eq(roleActions.roleId, userOrgRoleId!), | ||||||
|                     inArray(roleActions.roleId, userRoleIds!) |                     eq(roleActions.orgId, req.userOrgId!) | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             .limit(1); |             .limit(1); | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import db from "@server/db"; | import db from "@server/db"; | ||||||
| import { and, eq, inArray } from "drizzle-orm"; | import { and, eq } from "drizzle-orm"; | ||||||
| import { roleResources, userResources } from "@server/db/schemas"; | import { roleResources, userResources } from "@server/db/schemas"; | ||||||
| 
 | 
 | ||||||
| export async function canUserAccessResource({ | export async function canUserAccessResource({ | ||||||
|     userId, |     userId, | ||||||
|     resourceId, |     resourceId, | ||||||
|     roleIds |     roleId | ||||||
| }: { | }: { | ||||||
|     userId: string; |     userId: string; | ||||||
|     resourceId: number; |     resourceId: number; | ||||||
|     roleIds: number[]; |     roleId: number; | ||||||
| }): Promise<boolean> { | }): Promise<boolean> { | ||||||
|     const roleResourceAccess = await db |     const roleResourceAccess = await db | ||||||
|         .select() |         .select() | ||||||
|  | @ -17,7 +17,7 @@ export async function canUserAccessResource({ | ||||||
|         .where( |         .where( | ||||||
|             and( |             and( | ||||||
|                 eq(roleResources.resourceId, resourceId), |                 eq(roleResources.resourceId, resourceId), | ||||||
|                 inArray(roleResources.roleId, roleIds) |                 eq(roleResources.roleId, roleId) | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         .limit(1); |         .limit(1); | ||||||
|  |  | ||||||
|  | @ -417,6 +417,15 @@ export const resourceRules = sqliteTable("resourceRules", { | ||||||
|     value: text("value").notNull() |     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
 | // Identity Providers
 | ||||||
| export const idp = sqliteTable("idp", { | export const idp = sqliteTable("idp", { | ||||||
|     idpId: integer("idpId").primaryKey({ autoIncrement: true }), |     idpId: integer("idpId").primaryKey({ autoIncrement: true }), | ||||||
|  | @ -449,6 +458,12 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { | ||||||
|     scopes: text("scopes").notNull() |     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", { | export const hostMeta = sqliteTable("hostMeta", { | ||||||
|     hostMetaId: text("hostMetaId").primaryKey().notNull(), |     hostMetaId: text("hostMetaId").primaryKey().notNull(), | ||||||
|     createdAt: integer("createdAt").notNull() |     createdAt: integer("createdAt").notNull() | ||||||
|  | @ -528,8 +543,8 @@ export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>; | ||||||
| export type VersionMigration = InferSelectModel<typeof versionMigrations>; | export type VersionMigration = InferSelectModel<typeof versionMigrations>; | ||||||
| export type ResourceRule = InferSelectModel<typeof resourceRules>; | export type ResourceRule = InferSelectModel<typeof resourceRules>; | ||||||
| export type Domain = InferSelectModel<typeof domains>; | export type Domain = InferSelectModel<typeof domains>; | ||||||
|  | export type SupporterKey = InferSelectModel<typeof supporterKey>; | ||||||
| export type Idp = InferSelectModel<typeof idp>; | export type Idp = InferSelectModel<typeof idp>; | ||||||
| export type IdpOrg = InferSelectModel<typeof idpOrg>; |  | ||||||
| export type ApiKey = InferSelectModel<typeof apiKeys>; | export type ApiKey = InferSelectModel<typeof apiKeys>; | ||||||
| export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>; | export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>; | ||||||
| export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>; | export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>; | ||||||
|  |  | ||||||
|  | @ -5,7 +5,8 @@ import { createApiServer } from "./apiServer"; | ||||||
| import { createNextServer } from "./nextServer"; | import { createNextServer } from "./nextServer"; | ||||||
| import { createInternalServer } from "./internalServer"; | import { createInternalServer } from "./internalServer"; | ||||||
| import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; | import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; | ||||||
| // import { createIntegrationApiServer } from "./integrationApiServer";
 | import { createIntegrationApiServer } from "./integrationApiServer"; | ||||||
|  | import license from "./license/license.js"; | ||||||
| 
 | 
 | ||||||
| async function startServers() { | async function startServers() { | ||||||
|     await runSetupFunctions(); |     await runSetupFunctions(); | ||||||
|  | @ -16,7 +17,9 @@ async function startServers() { | ||||||
|     const nextServer = await createNextServer(); |     const nextServer = await createNextServer(); | ||||||
| 
 | 
 | ||||||
|     let integrationServer; |     let integrationServer; | ||||||
|     // integrationServer = createIntegrationApiServer();
 |     if (await license.isUnlocked()) { | ||||||
|  |         integrationServer = createIntegrationApiServer(); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         apiServer, |         apiServer, | ||||||
|  | @ -35,7 +38,7 @@ declare global { | ||||||
|             session?: Session; |             session?: Session; | ||||||
|             userOrg?: UserOrg; |             userOrg?: UserOrg; | ||||||
|             apiKeyOrg?: ApiKeyOrg; |             apiKeyOrg?: ApiKeyOrg; | ||||||
|             userRoleIds?: number[]; |             userOrgRoleId?: number; | ||||||
|             userOrgId?: string; |             userOrgId?: string; | ||||||
|             userOrgIds?: string[]; |             userOrgIds?: string[]; | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										110
									
								
								server/integrationApiServer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								server/integrationApiServer.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,110 @@ | ||||||
|  | // 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 db from "@server/db"; | ||||||
| import { and, eq, inArray } from "drizzle-orm"; | import { and, eq } from "drizzle-orm"; | ||||||
| import { roleResources, userResources } from "@server/db/schemas"; | import { roleResources, userResources } from "@server/db/schemas"; | ||||||
| 
 | 
 | ||||||
| export async function canUserAccessResource({ | export async function canUserAccessResource({ | ||||||
|     userId, |     userId, | ||||||
|     resourceId, |     resourceId, | ||||||
|     roleIds |     roleId | ||||||
| }: { | }: { | ||||||
|     userId: string; |     userId: string; | ||||||
|     resourceId: number; |     resourceId: number; | ||||||
|     roleIds: number[]; |     roleId: number; | ||||||
| }): Promise<boolean> { | }): Promise<boolean> { | ||||||
|     const roleResourceAccess = await db |     const roleResourceAccess = await db | ||||||
|         .select() |         .select() | ||||||
|  | @ -17,7 +17,7 @@ export async function canUserAccessResource({ | ||||||
|         .where( |         .where( | ||||||
|             and( |             and( | ||||||
|                 eq(roleResources.resourceId, resourceId), |                 eq(roleResources.resourceId, resourceId), | ||||||
|                 inArray(roleResources.roleId, roleIds) |                 eq(roleResources.roleId, roleId) | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         .limit(1); |         .limit(1); | ||||||
|  |  | ||||||
|  | @ -10,6 +10,10 @@ import { | ||||||
| } from "@server/lib/consts"; | } from "@server/lib/consts"; | ||||||
| import { passwordSchema } from "@server/auth/passwordSchema"; | import { passwordSchema } from "@server/auth/passwordSchema"; | ||||||
| import stoi from "./stoi"; | 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); | const portSchema = z.number().positive().gt(0).lte(65535); | ||||||
| 
 | 
 | ||||||
|  | @ -168,6 +172,10 @@ const configSchema = z.object({ | ||||||
| export class Config { | export class Config { | ||||||
|     private rawConfig!: z.infer<typeof configSchema>; |     private rawConfig!: z.infer<typeof configSchema>; | ||||||
| 
 | 
 | ||||||
|  |     supporterData: SupporterKey | null = null; | ||||||
|  | 
 | ||||||
|  |     supporterHiddenUntil: number | null = null; | ||||||
|  | 
 | ||||||
|     isDev: boolean = process.env.ENVIRONMENT !== "prod"; |     isDev: boolean = process.env.ENVIRONMENT !== "prod"; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|  | @ -256,9 +264,20 @@ export class Config { | ||||||
|             : "false"; |             : "false"; | ||||||
|         process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; |         process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; | ||||||
| 
 | 
 | ||||||
|  |         license.setServerSecret(parsedConfig.data.server.secret); | ||||||
|  | 
 | ||||||
|  |         this.checkKeyStatus(); | ||||||
|  | 
 | ||||||
|         this.rawConfig = parsedConfig.data; |         this.rawConfig = parsedConfig.data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private async checkKeyStatus() { | ||||||
|  |         const licenseStatus = await license.check(); | ||||||
|  |         if (!licenseStatus.isHostLicensed) { | ||||||
|  |             this.checkSupporterKey(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public getRawConfig() { |     public getRawConfig() { | ||||||
|         return this.rawConfig; |         return this.rawConfig; | ||||||
|     } |     } | ||||||
|  | @ -272,6 +291,90 @@ export class Config { | ||||||
|     public getDomain(domainId: string) { |     public getDomain(domainId: string) { | ||||||
|         return this.rawConfig.domains[domainId]; |         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(); | export const config = new Config(); | ||||||
|  |  | ||||||
							
								
								
									
										493
									
								
								server/license/license.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										493
									
								
								server/license/license.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,493 @@ | ||||||
|  | // 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; | ||||||
							
								
								
									
										114
									
								
								server/license/licenseJwt.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								server/license/licenseJwt.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,114 @@ | ||||||
|  | // 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,6 +16,7 @@ export * from "./verifyUserInRole"; | ||||||
| export * from "./verifyAccessTokenAccess"; | export * from "./verifyAccessTokenAccess"; | ||||||
| export * from "./verifyUserIsServerAdmin"; | export * from "./verifyUserIsServerAdmin"; | ||||||
| export * from "./verifyIsLoggedInUser"; | export * from "./verifyIsLoggedInUser"; | ||||||
| // export * from "./integration";
 | export * from "./integration"; | ||||||
|  | export * from "./verifyValidLicense"; | ||||||
| export * from "./verifyUserHasAction"; | export * from "./verifyUserHasAction"; | ||||||
| // export * from "./verifyApiKeyAccess";
 | export * from "./verifyApiKeyAccess"; | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								server/middlewares/integration/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/middlewares/integration/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | // 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"; | ||||||
							
								
								
									
										115
									
								
								server/middlewares/integration/verifyAccessTokenAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								server/middlewares/integration/verifyAccessTokenAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								server/middlewares/integration/verifyApiKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								server/middlewares/integration/verifyApiKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								server/middlewares/integration/verifyApiKeyApiKeyAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								server/middlewares/integration/verifyApiKeyApiKeyAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								server/middlewares/integration/verifyApiKeyHasAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								server/middlewares/integration/verifyApiKeyHasAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | // 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" | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								server/middlewares/integration/verifyApiKeyIsRoot.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/middlewares/integration/verifyApiKeyIsRoot.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										66
									
								
								server/middlewares/integration/verifyApiKeyOrgAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								server/middlewares/integration/verifyApiKeyOrgAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								server/middlewares/integration/verifyApiKeyResourceAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								server/middlewares/integration/verifyApiKeyResourceAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								server/middlewares/integration/verifyApiKeyRoleAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								server/middlewares/integration/verifyApiKeyRoleAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,74 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										94
									
								
								server/middlewares/integration/verifyApiKeySiteAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								server/middlewares/integration/verifyApiKeySiteAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								server/middlewares/integration/verifyApiKeyTargetAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								server/middlewares/integration/verifyApiKeyTargetAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								server/middlewares/integration/verifyApiKeyUserAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								server/middlewares/integration/verifyApiKeyUserAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | ||||||
|  | // 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.userOrg = res[0]; | ||||||
|             req.userRoleIds = res.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|             return next( |             next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.FORBIDDEN, |                     HttpCode.FORBIDDEN, | ||||||
|                     "User does not have access to this organization" |                     "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({ |         const resourceAllowed = await canUserAccessResource({ | ||||||
|             userId, |             userId, | ||||||
|             resourceId, |             resourceId, | ||||||
|             roleIds: req.userRoleIds! |             roleId: req.userOrgRoleId! | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (!resourceAllowed) { |         if (!resourceAllowed) { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Request, Response, NextFunction } from "express"; | import { Request, Response, NextFunction } from "express"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
| import { roles, userOrgs } from "@server/db/schemas"; | import { roles, userOrgs } from "@server/db/schemas"; | ||||||
| import { and, eq, inArray } from "drizzle-orm"; | import { and, eq } from "drizzle-orm"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| 
 | 
 | ||||||
|  | @ -29,11 +29,9 @@ export async function verifyAdmin( | ||||||
|         const userOrgRes = await db |         const userOrgRes = await db | ||||||
|             .select() |             .select() | ||||||
|             .from(userOrgs) |             .from(userOrgs) | ||||||
|             .where( |             .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!))) | ||||||
|                 and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)) |             .limit(1); | ||||||
|             ); |  | ||||||
|         req.userOrg = userOrgRes[0]; |         req.userOrg = userOrgRes[0]; | ||||||
|         req.userRoleIds = userOrgRes.map((r) => r.roleId); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!req.userOrg) { |     if (!req.userOrg) { | ||||||
|  | @ -45,13 +43,13 @@ export async function verifyAdmin( | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const userAdminRole = await db |     const userRole = await db | ||||||
|         .select() |         .select() | ||||||
|         .from(roles) |         .from(roles) | ||||||
|         .where(and(inArray(roles.roleId, req.userRoleIds!), roles.isAdmin)) |         .where(eq(roles.roleId, req.userOrg.roleId)) | ||||||
|         .limit(1); |         .limit(1); | ||||||
| 
 | 
 | ||||||
|     if (userAdminRole.length === 0) { |     if (userRole.length === 0 || !userRole[0].isAdmin) { | ||||||
|         return next( |         return next( | ||||||
|             createHttpError( |             createHttpError( | ||||||
|                 HttpCode.FORBIDDEN, |                 HttpCode.FORBIDDEN, | ||||||
|  |  | ||||||
							
								
								
									
										104
									
								
								server/middlewares/verifyApiKeyAccess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								server/middlewares/verifyApiKeyAccess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,104 @@ | ||||||
|  | // 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,20 +34,21 @@ export async function verifyOrgAccess( | ||||||
|                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) |                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) | ||||||
|                 ); |                 ); | ||||||
|             req.userOrg = userOrgRes[0]; |             req.userOrg = userOrgRes[0]; | ||||||
|             req.userRoleIds = userOrgRes.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|             return next( |             next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.FORBIDDEN, |                     HttpCode.FORBIDDEN, | ||||||
|                     "User does not have access to this organization" |                     "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; |             req.userOrgId = orgId; | ||||||
|             return next(); |             return next(); | ||||||
|  |         } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         return next( |         return next( | ||||||
|             createHttpError( |             createHttpError( | ||||||
|  |  | ||||||
|  | @ -4,9 +4,9 @@ import { | ||||||
|     resources, |     resources, | ||||||
|     userOrgs, |     userOrgs, | ||||||
|     userResources, |     userResources, | ||||||
|     roleResources |     roleResources, | ||||||
| } from "@server/db/schemas"; | } from "@server/db/schemas"; | ||||||
| import { and, eq, inArray } from "drizzle-orm"; | import { and, eq } from "drizzle-orm"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| 
 | 
 | ||||||
|  | @ -59,9 +59,9 @@ export async function verifyResourceAccess( | ||||||
|                         eq(userOrgs.userId, userId), |                         eq(userOrgs.userId, userId), | ||||||
|                         eq(userOrgs.orgId, resource[0].orgId) |                         eq(userOrgs.orgId, resource[0].orgId) | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ) | ||||||
|  |                 .limit(1); | ||||||
|             req.userOrg = userOrgRole[0]; |             req.userOrg = userOrgRole[0]; | ||||||
|             req.userRoleIds = userOrgRole.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|  | @ -73,6 +73,8 @@ export async function verifyResourceAccess( | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         const userOrgRoleId = req.userOrg.roleId; | ||||||
|  |         req.userOrgRoleId = userOrgRoleId; | ||||||
|         req.userOrgId = resource[0].orgId; |         req.userOrgId = resource[0].orgId; | ||||||
| 
 | 
 | ||||||
|         const roleResourceAccess = await db |         const roleResourceAccess = await db | ||||||
|  | @ -81,7 +83,7 @@ export async function verifyResourceAccess( | ||||||
|             .where( |             .where( | ||||||
|                 and( |                 and( | ||||||
|                     eq(roleResources.resourceId, resourceId), |                     eq(roleResources.resourceId, resourceId), | ||||||
|                     inArray(roleResources.roleId, req.userRoleIds!) |                     eq(roleResources.roleId, userOrgRoleId) | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             .limit(1); |             .limit(1); | ||||||
|  |  | ||||||
|  | @ -98,10 +98,11 @@ export async function verifyRoleAccess( | ||||||
|                 .from(userOrgs) |                 .from(userOrgs) | ||||||
|                 .where( |                 .where( | ||||||
|                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)) |                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)) | ||||||
|                 ); |                 ) | ||||||
|  |                 .limit(1); | ||||||
| 
 | 
 | ||||||
|             req.userOrg = userOrg[0]; |             req.userOrg = userOrg[0]; | ||||||
|             req.userRoleIds = userOrg.map((r) => r.roleId); |             req.userOrgRoleId = userOrg[0].roleId; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return next(); |         return next(); | ||||||
|  |  | ||||||
|  | @ -5,9 +5,9 @@ import { | ||||||
|     userOrgs, |     userOrgs, | ||||||
|     userSites, |     userSites, | ||||||
|     roleSites, |     roleSites, | ||||||
|     roles |     roles, | ||||||
| } from "@server/db/schemas"; | } from "@server/db/schemas"; | ||||||
| import { and, eq, inArray } from "drizzle-orm"; | import { and, eq, or } from "drizzle-orm"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| import logger from "@server/logger"; | import logger from "@server/logger"; | ||||||
|  | @ -71,7 +71,6 @@ export async function verifySiteAccess( | ||||||
|                 ) |                 ) | ||||||
|                 .limit(1); |                 .limit(1); | ||||||
|             req.userOrg = userOrgRole[0]; |             req.userOrg = userOrgRole[0]; | ||||||
|             req.userRoleIds = userOrgRole.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|  | @ -83,6 +82,8 @@ export async function verifySiteAccess( | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         const userOrgRoleId = req.userOrg.roleId; | ||||||
|  |         req.userOrgRoleId = userOrgRoleId; | ||||||
|         req.userOrgId = site[0].orgId; |         req.userOrgId = site[0].orgId; | ||||||
| 
 | 
 | ||||||
|         // Check role-based site access first
 |         // Check role-based site access first
 | ||||||
|  | @ -92,7 +93,7 @@ export async function verifySiteAccess( | ||||||
|             .where( |             .where( | ||||||
|                 and( |                 and( | ||||||
|                     eq(roleSites.siteId, siteId), |                     eq(roleSites.siteId, siteId), | ||||||
|                     inArray(roleSites.roleId, req.userRoleIds!) |                     eq(roleSites.roleId, userOrgRoleId) | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             .limit(1); |             .limit(1); | ||||||
|  |  | ||||||
|  | @ -88,23 +88,24 @@ export async function verifyTargetAccess( | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ); | ||||||
|             req.userOrg = res[0]; |             req.userOrg = res[0]; | ||||||
|             req.userRoleIds = res.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|             return next( |             next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.FORBIDDEN, |                     HttpCode.FORBIDDEN, | ||||||
|                     "User does not have access to this organization" |                     "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({ |         const resourceAllowed = await canUserAccessResource({ | ||||||
|             userId, |             userId, | ||||||
|             resourceId, |             resourceId, | ||||||
|             roleIds: req.userRoleIds! |             roleId: req.userOrgRoleId! | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (!resourceAllowed) { |         if (!resourceAllowed) { | ||||||
|  |  | ||||||
|  | @ -33,9 +33,9 @@ export async function verifyUserAccess( | ||||||
|                         eq(userOrgs.userId, reqUserId), |                         eq(userOrgs.userId, reqUserId), | ||||||
|                         eq(userOrgs.orgId, req.userOrgId!) |                         eq(userOrgs.orgId, req.userOrgId!) | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ) | ||||||
|  |                 .limit(1); | ||||||
|             req.userOrg = res[0]; |             req.userOrg = res[0]; | ||||||
|             req.userRoleIds = res.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ export async function verifyUserInRole( | ||||||
|         const roleId = parseInt( |         const roleId = parseInt( | ||||||
|             req.params.roleId || req.body.roleId || req.query.roleId |             req.params.roleId || req.body.roleId || req.query.roleId | ||||||
|         ); |         ); | ||||||
|         const userRoleIds = req.userRoleIds; |         const userRoleId = req.userOrgRoleId; | ||||||
| 
 | 
 | ||||||
|         if (isNaN(roleId)) { |         if (isNaN(roleId)) { | ||||||
|             return next( |             return next( | ||||||
|  | @ -20,7 +20,7 @@ export async function verifyUserInRole( | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!userRoleIds) { |         if (!userRoleId) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.FORBIDDEN, |                     HttpCode.FORBIDDEN, | ||||||
|  | @ -29,7 +29,7 @@ export async function verifyUserInRole( | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (userRoleIds.indexOf(roleId) === -1) { |         if (userRoleId !== roleId) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.FORBIDDEN, |                     HttpCode.FORBIDDEN, | ||||||
|  |  | ||||||
|  | @ -36,7 +36,6 @@ export async function verifyUserIsOrgOwner( | ||||||
|                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) |                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) | ||||||
|                 ); |                 ); | ||||||
|             req.userOrg = res[0]; |             req.userOrg = res[0]; | ||||||
|             req.userRoleIds = res.map((r) => r.roleId); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!req.userOrg) { |         if (!req.userOrg) { | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								server/middlewares/verifyValidLicense.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								server/middlewares/verifyValidLicense.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | // 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( |                 .where( | ||||||
|                     or( |                     or( | ||||||
|                         eq(userResources.userId, req.user!.userId), |                         eq(userResources.userId, req.user!.userId), | ||||||
|                         inArray(roleResources.roleId, req.userRoleIds!) |                         eq(roleResources.roleId, req.userOrgRoleId!) | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ); | ||||||
|         } else { |         } else { | ||||||
|  |  | ||||||
							
								
								
									
										133
									
								
								server/routers/apiKeys/createOrgApiKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								server/routers/apiKeys/createOrgApiKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,133 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										105
									
								
								server/routers/apiKeys/createRootApiKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								server/routers/apiKeys/createRootApiKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | ||||||
|  | // 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" | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								server/routers/apiKeys/deleteApiKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								server/routers/apiKeys/deleteApiKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										104
									
								
								server/routers/apiKeys/deleteOrgApiKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								server/routers/apiKeys/deleteOrgApiKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,104 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								server/routers/apiKeys/getApiKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								server/routers/apiKeys/getApiKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								server/routers/apiKeys/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/routers/apiKeys/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | // 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"; | ||||||
							
								
								
									
										118
									
								
								server/routers/apiKeys/listApiKeyActions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								server/routers/apiKeys/listApiKeyActions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										121
									
								
								server/routers/apiKeys/listOrgApiKeys.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								server/routers/apiKeys/listOrgApiKeys.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,121 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								server/routers/apiKeys/listRootApiKeys.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								server/routers/apiKeys/listRootApiKeys.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										141
									
								
								server/routers/apiKeys/setApiKeyActions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								server/routers/apiKeys/setApiKeyActions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,141 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										122
									
								
								server/routers/apiKeys/setApiKeyOrgs.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								server/routers/apiKeys/setApiKeyOrgs.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,122 @@ | ||||||
|  | // 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,9 +8,11 @@ import * as target from "./target"; | ||||||
| import * as user from "./user"; | import * as user from "./user"; | ||||||
| import * as auth from "./auth"; | import * as auth from "./auth"; | ||||||
| import * as role from "./role"; | import * as role from "./role"; | ||||||
|  | import * as supporterKey from "./supporterKey"; | ||||||
| import * as accessToken from "./accessToken"; | import * as accessToken from "./accessToken"; | ||||||
| import * as idp from "./idp"; | import * as idp from "./idp"; | ||||||
| // import * as apiKeys from "./apiKeys";
 | import * as license from "./license"; | ||||||
|  | import * as apiKeys from "./apiKeys"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| import { | import { | ||||||
|     verifyAccessTokenAccess, |     verifyAccessTokenAccess, | ||||||
|  | @ -26,8 +28,9 @@ import { | ||||||
|     verifyUserAccess, |     verifyUserAccess, | ||||||
|     getUserOrgs, |     getUserOrgs, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     verifyIsLoggedInUser |     verifyIsLoggedInUser, | ||||||
|     // verifyApiKeyAccess
 |     verifyApiKeyAccess, | ||||||
|  |     verifyValidLicense | ||||||
| } from "@server/middlewares"; | } from "@server/middlewares"; | ||||||
| import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; | import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; | ||||||
| import { ActionsEnum } from "@server/auth/actions"; | import { ActionsEnum } from "@server/auth/actions"; | ||||||
|  | @ -273,14 +276,6 @@ authenticated.get( | ||||||
|     verifyUserHasAction(ActionsEnum.listRoles), |     verifyUserHasAction(ActionsEnum.listRoles), | ||||||
|     role.listRoles |     role.listRoles | ||||||
| ); | ); | ||||||
| authenticated.post( |  | ||||||
|     "/org/:orgId/user/:userId/roles", |  | ||||||
|     verifyOrgAccess, |  | ||||||
|     verifyUserAccess, |  | ||||||
|     verifyUserHasAction(ActionsEnum.setUserRoles), |  | ||||||
|     user.setUserRoles |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| // authenticated.get(
 | // authenticated.get(
 | ||||||
| //     "/role/:roleId",
 | //     "/role/:roleId",
 | ||||||
| //     verifyRoleAccess,
 | //     verifyRoleAccess,
 | ||||||
|  | @ -411,6 +406,12 @@ authenticated.get( | ||||||
| 
 | 
 | ||||||
| authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview); | 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); | unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); | ||||||
| 
 | 
 | ||||||
| // authenticated.get(
 | // authenticated.get(
 | ||||||
|  | @ -530,24 +531,28 @@ authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); | ||||||
| 
 | 
 | ||||||
| authenticated.put( | authenticated.put( | ||||||
|     "/idp/:idpId/org/:orgId", |     "/idp/:idpId/org/:orgId", | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     idp.createIdpOrgPolicy |     idp.createIdpOrgPolicy | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.post( | authenticated.post( | ||||||
|     "/idp/:idpId/org/:orgId", |     "/idp/:idpId/org/:orgId", | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     idp.updateIdpOrgPolicy |     idp.updateIdpOrgPolicy | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.delete( | authenticated.delete( | ||||||
|     "/idp/:idpId/org/:orgId", |     "/idp/:idpId/org/:orgId", | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     idp.deleteIdpOrgPolicy |     idp.deleteIdpOrgPolicy | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.get( | authenticated.get( | ||||||
|     "/idp/:idpId/org", |     "/idp/:idpId/org", | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     idp.listIdpOrgPolicies |     idp.listIdpOrgPolicies | ||||||
| ); | ); | ||||||
|  | @ -555,45 +560,75 @@ authenticated.get( | ||||||
| authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
 | 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.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( | authenticated.get( | ||||||
|     `/api-key/:apiKeyId`, |     `/api-key/:apiKeyId`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     apiKeys.getApiKey |     apiKeys.getApiKey | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.put( | authenticated.put( | ||||||
|     `/api-key`, |     `/api-key`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     apiKeys.createRootApiKey |     apiKeys.createRootApiKey | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.delete( | authenticated.delete( | ||||||
|     `/api-key/:apiKeyId`, |     `/api-key/:apiKeyId`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     apiKeys.deleteApiKey |     apiKeys.deleteApiKey | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.get( | authenticated.get( | ||||||
|     `/api-keys`, |     `/api-keys`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     apiKeys.listRootApiKeys |     apiKeys.listRootApiKeys | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.get( | authenticated.get( | ||||||
|     `/api-key/:apiKeyId/actions`, |     `/api-key/:apiKeyId/actions`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     apiKeys.listApiKeyActions |     apiKeys.listApiKeyActions | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.post( | authenticated.post( | ||||||
|     `/api-key/:apiKeyId/actions`, |     `/api-key/:apiKeyId/actions`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyUserIsServerAdmin, |     verifyUserIsServerAdmin, | ||||||
|     apiKeys.setApiKeyActions |     apiKeys.setApiKeyActions | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| authenticated.get( | authenticated.get( | ||||||
|     `/org/:orgId/api-keys`, |     `/org/:orgId/api-keys`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyOrgAccess, |     verifyOrgAccess, | ||||||
|     verifyUserHasAction(ActionsEnum.listApiKeys), |     verifyUserHasAction(ActionsEnum.listApiKeys), | ||||||
|     apiKeys.listOrgApiKeys |     apiKeys.listOrgApiKeys | ||||||
|  | @ -601,6 +636,7 @@ authenticated.get( | ||||||
| 
 | 
 | ||||||
| authenticated.post( | authenticated.post( | ||||||
|     `/org/:orgId/api-key/:apiKeyId/actions`, |     `/org/:orgId/api-key/:apiKeyId/actions`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyOrgAccess, |     verifyOrgAccess, | ||||||
|     verifyApiKeyAccess, |     verifyApiKeyAccess, | ||||||
|     verifyUserHasAction(ActionsEnum.setApiKeyActions), |     verifyUserHasAction(ActionsEnum.setApiKeyActions), | ||||||
|  | @ -609,6 +645,7 @@ authenticated.post( | ||||||
| 
 | 
 | ||||||
| authenticated.get( | authenticated.get( | ||||||
|     `/org/:orgId/api-key/:apiKeyId/actions`, |     `/org/:orgId/api-key/:apiKeyId/actions`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyOrgAccess, |     verifyOrgAccess, | ||||||
|     verifyApiKeyAccess, |     verifyApiKeyAccess, | ||||||
|     verifyUserHasAction(ActionsEnum.listApiKeyActions), |     verifyUserHasAction(ActionsEnum.listApiKeyActions), | ||||||
|  | @ -617,6 +654,7 @@ authenticated.get( | ||||||
| 
 | 
 | ||||||
| authenticated.put( | authenticated.put( | ||||||
|     `/org/:orgId/api-key`, |     `/org/:orgId/api-key`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyOrgAccess, |     verifyOrgAccess, | ||||||
|     verifyUserHasAction(ActionsEnum.createApiKey), |     verifyUserHasAction(ActionsEnum.createApiKey), | ||||||
|     apiKeys.createOrgApiKey |     apiKeys.createOrgApiKey | ||||||
|  | @ -624,6 +662,7 @@ authenticated.put( | ||||||
| 
 | 
 | ||||||
| authenticated.delete( | authenticated.delete( | ||||||
|     `/org/:orgId/api-key/:apiKeyId`, |     `/org/:orgId/api-key/:apiKeyId`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyOrgAccess, |     verifyOrgAccess, | ||||||
|     verifyApiKeyAccess, |     verifyApiKeyAccess, | ||||||
|     verifyUserHasAction(ActionsEnum.deleteApiKey), |     verifyUserHasAction(ActionsEnum.deleteApiKey), | ||||||
|  | @ -632,12 +671,12 @@ authenticated.delete( | ||||||
| 
 | 
 | ||||||
| authenticated.get( | authenticated.get( | ||||||
|     `/org/:orgId/api-key/:apiKeyId`, |     `/org/:orgId/api-key/:apiKeyId`, | ||||||
|  |     verifyValidLicense, | ||||||
|     verifyOrgAccess, |     verifyOrgAccess, | ||||||
|     verifyApiKeyAccess, |     verifyApiKeyAccess, | ||||||
|     verifyUserHasAction(ActionsEnum.getApiKey), |     verifyUserHasAction(ActionsEnum.getApiKey), | ||||||
|     apiKeys.getApiKey |     apiKeys.getApiKey | ||||||
| ); | ); | ||||||
| */ |  | ||||||
| 
 | 
 | ||||||
| // Auth routes
 | // Auth routes
 | ||||||
| export const authRouter = Router(); | export const authRouter = Router(); | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | // 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 { Request, Response, NextFunction } from "express"; | ||||||
| import { z } from "zod"; | import { z } from "zod"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
|  | @ -7,6 +12,7 @@ import createHttpError from "http-errors"; | ||||||
| import logger from "@server/logger"; | import logger from "@server/logger"; | ||||||
| import { fromError } from "zod-validation-error"; | import { fromError } from "zod-validation-error"; | ||||||
| import { OpenAPITags, registry } from "@server/openApi"; | import { OpenAPITags, registry } from "@server/openApi"; | ||||||
|  | import config from "@server/lib/config"; | ||||||
| import { eq, and } from "drizzle-orm"; | import { eq, and } from "drizzle-orm"; | ||||||
| import { idp, idpOrg } from "@server/db/schemas"; | import { idp, idpOrg } from "@server/db/schemas"; | ||||||
| 
 | 
 | ||||||
|  | @ -50,17 +56,6 @@ export async function createIdpOrgPolicy( | ||||||
|     next: NextFunction |     next: NextFunction | ||||||
| ): Promise<any> { | ): Promise<any> { | ||||||
|     try { |     try { | ||||||
|         const parsedParams = paramsSchema.safeParse(req.params); |  | ||||||
|         if (!parsedParams.success) { |  | ||||||
|             return next( |  | ||||||
|                 createHttpError( |  | ||||||
|                     HttpCode.BAD_REQUEST, |  | ||||||
|                     fromError(parsedParams.error).toString() |  | ||||||
|                 ) |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         const { idpId, orgId } = parsedParams.data; |  | ||||||
| 
 |  | ||||||
|         const parsedBody = bodySchema.safeParse(req.body); |         const parsedBody = bodySchema.safeParse(req.body); | ||||||
|         if (!parsedBody.success) { |         if (!parsedBody.success) { | ||||||
|             return next( |             return next( | ||||||
|  | @ -70,9 +65,20 @@ export async function createIdpOrgPolicy( | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         let { orgMapping, roleMapping } = parsedBody.data; |  | ||||||
| 
 | 
 | ||||||
|         // Given identity provider must exist and not have a policy already
 |         const parsedParams = paramsSchema.safeParse(req.params); | ||||||
|  |         if (!parsedParams.success) { | ||||||
|  |             return next( | ||||||
|  |                 createHttpError( | ||||||
|  |                     HttpCode.BAD_REQUEST, | ||||||
|  |                     fromError(parsedParams.error).toString() | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const { idpId, orgId } = parsedParams.data; | ||||||
|  |         const { roleMapping, orgMapping } = parsedBody.data; | ||||||
|  | 
 | ||||||
|         const [existing] = await db |         const [existing] = await db | ||||||
|             .select() |             .select() | ||||||
|             .from(idp) |             .from(idp) | ||||||
|  | @ -84,15 +90,18 @@ export async function createIdpOrgPolicy( | ||||||
| 
 | 
 | ||||||
|         if (!existing?.idp) { |         if (!existing?.idp) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError(HttpCode.NOT_FOUND, "Idp does not exist") |                 createHttpError( | ||||||
|  |                     HttpCode.BAD_REQUEST, | ||||||
|  |                     "An IDP with this ID does not exist." | ||||||
|  |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (existing.idpOrg) { |         if (existing.idpOrg) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.CONFLICT, |                     HttpCode.BAD_REQUEST, | ||||||
|                     "Org policy already exists for this idp" |                     "An IDP org policy already exists." | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  | @ -100,15 +109,15 @@ export async function createIdpOrgPolicy( | ||||||
|         await db.insert(idpOrg).values({ |         await db.insert(idpOrg).values({ | ||||||
|             idpId, |             idpId, | ||||||
|             orgId, |             orgId, | ||||||
|             orgMapping, |             roleMapping, | ||||||
|             roleMapping |             orgMapping | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return response<CreateIdpOrgPolicyResponse>(res, { |         return response<CreateIdpOrgPolicyResponse>(res, { | ||||||
|             data: {}, |             data: {}, | ||||||
|             success: true, |             success: true, | ||||||
|             error: false, |             error: false, | ||||||
|             message: "Idp org policy created successfully", |             message: "Idp created successfully", | ||||||
|             status: HttpCode.CREATED |             status: HttpCode.CREATED | ||||||
|         }); |         }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas"; | ||||||
| import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; | import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; | ||||||
| import { encrypt } from "@server/lib/crypto"; | import { encrypt } from "@server/lib/crypto"; | ||||||
| import config from "@server/lib/config"; | import config from "@server/lib/config"; | ||||||
|  | import license from "@server/license/license"; | ||||||
| 
 | 
 | ||||||
| const paramsSchema = z.object({}).strict(); | const paramsSchema = z.object({}).strict(); | ||||||
| 
 | 
 | ||||||
|  | @ -80,6 +81,10 @@ export async function createOidcIdp( | ||||||
|             autoProvision |             autoProvision | ||||||
|         } = parsedBody.data; |         } = parsedBody.data; | ||||||
| 
 | 
 | ||||||
|  |         if (!(await license.isUnlocked())) { | ||||||
|  |             autoProvision = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const key = config.getRawConfig().server.secret; |         const key = config.getRawConfig().server.secret; | ||||||
| 
 | 
 | ||||||
|         const encryptedSecret = encrypt(clientSecret, key); |         const encryptedSecret = encrypt(clientSecret, key); | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | // 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 { Request, Response, NextFunction } from "express"; | ||||||
| import { z } from "zod"; | import { z } from "zod"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
|  | @ -6,7 +11,7 @@ import HttpCode from "@server/types/HttpCode"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import logger from "@server/logger"; | import logger from "@server/logger"; | ||||||
| import { fromError } from "zod-validation-error"; | import { fromError } from "zod-validation-error"; | ||||||
| import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; | import { idp, idpOrg } from "@server/db/schemas"; | ||||||
| import { eq, and } from "drizzle-orm"; | import { eq, and } from "drizzle-orm"; | ||||||
| import { OpenAPITags, registry } from "@server/openApi"; | import { OpenAPITags, registry } from "@server/openApi"; | ||||||
| 
 | 
 | ||||||
|  | @ -20,7 +25,7 @@ const paramsSchema = z | ||||||
| registry.registerPath({ | registry.registerPath({ | ||||||
|     method: "delete", |     method: "delete", | ||||||
|     path: "/idp/{idpId}/org/{orgId}", |     path: "/idp/{idpId}/org/{orgId}", | ||||||
|     description: "Delete an IDP policy for an IDP on an organization.", |     description: "Create an OIDC IdP for an organization.", | ||||||
|     tags: [OpenAPITags.Idp], |     tags: [OpenAPITags.Idp], | ||||||
|     request: { |     request: { | ||||||
|         params: paramsSchema |         params: paramsSchema | ||||||
|  | @ -46,27 +51,26 @@ export async function deleteIdpOrgPolicy( | ||||||
| 
 | 
 | ||||||
|         const { idpId, orgId } = parsedParams.data; |         const { idpId, orgId } = parsedParams.data; | ||||||
| 
 | 
 | ||||||
|         // Check if IDP policy, exists
 |  | ||||||
|         const [existing] = await db |         const [existing] = await db | ||||||
|             .select() |             .select() | ||||||
|             .from(idp) |             .from(idp) | ||||||
|             .leftJoin( |             .leftJoin(idpOrg, eq(idpOrg.orgId, orgId)) | ||||||
|                 idpOrg, |             .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))); | ||||||
|                 and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) |  | ||||||
|             ) |  | ||||||
|             .where(eq(idp.idpId, idpId)); |  | ||||||
| 
 | 
 | ||||||
|         if (!existing?.idp) { |         if (!existing.idp) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError(HttpCode.NOT_FOUND, "Idp does not exist") |                 createHttpError( | ||||||
|  |                     HttpCode.BAD_REQUEST, | ||||||
|  |                     "An IDP with this ID does not exist." | ||||||
|  |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!existing.idpOrg) { |         if (!existing.idpOrg) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.NOT_FOUND, |                     HttpCode.BAD_REQUEST, | ||||||
|                     "Org policy does not exist for this idp" |                     "A policy for this IDP and org does not exist." | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  | @ -79,7 +83,7 @@ export async function deleteIdpOrgPolicy( | ||||||
|             data: null, |             data: null, | ||||||
|             success: true, |             success: true, | ||||||
|             error: false, |             error: false, | ||||||
|             message: "Idp policy deleted successfully", |             message: "Policy deleted successfully", | ||||||
|             status: HttpCode.OK |             status: HttpCode.OK | ||||||
|         }); |         }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |  | ||||||
|  | @ -7,5 +7,5 @@ export * from "./validateOidcCallback"; | ||||||
| export * from "./getIdp"; | export * from "./getIdp"; | ||||||
| export * from "./createIdpOrgPolicy"; | export * from "./createIdpOrgPolicy"; | ||||||
| export * from "./deleteIdpOrgPolicy"; | export * from "./deleteIdpOrgPolicy"; | ||||||
| export * from "./updateIdpOrgPolicy"; |  | ||||||
| export * from "./listIdpOrgPolicies"; | export * from "./listIdpOrgPolicies"; | ||||||
|  | export * from "./updateIdpOrgPolicy"; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,12 @@ | ||||||
|  | // 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 { Request, Response, NextFunction } from "express"; | ||||||
| import { z } from "zod"; | import { z } from "zod"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
| import { idpOrg, type IdpOrg } from "@server/db/schemas"; | import { idpOrg } from "@server/db/schemas"; | ||||||
| import response from "@server/lib/response"; | import response from "@server/lib/response"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
|  | @ -10,11 +15,9 @@ import logger from "@server/logger"; | ||||||
| import { fromError } from "zod-validation-error"; | import { fromError } from "zod-validation-error"; | ||||||
| import { OpenAPITags, registry } from "@server/openApi"; | import { OpenAPITags, registry } from "@server/openApi"; | ||||||
| 
 | 
 | ||||||
| const paramsSchema = z | const paramsSchema = z.object({ | ||||||
|     .object({ |  | ||||||
|     idpId: z.coerce.number() |     idpId: z.coerce.number() | ||||||
|     }) | }); | ||||||
|     .strict(); |  | ||||||
| 
 | 
 | ||||||
| const querySchema = z | const querySchema = z | ||||||
|     .object({ |     .object({ | ||||||
|  | @ -44,12 +47,8 @@ async function query(idpId: number, limit: number, offset: number) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type ListIdpOrgPoliciesResponse = { | export type ListIdpOrgPoliciesResponse = { | ||||||
|     policies: Array<IdpOrg>; |     policies: NonNullable<Awaited<ReturnType<typeof query>>>; | ||||||
|     pagination: { |     pagination: { total: number; limit: number; offset: number }; | ||||||
|         total: number; |  | ||||||
|         limit: number; |  | ||||||
|         offset: number; |  | ||||||
|     }; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| registry.registerPath({ | registry.registerPath({ | ||||||
|  | @ -79,7 +78,6 @@ export async function listIdpOrgPolicies( | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         const { idpId } = parsedParams.data; |         const { idpId } = parsedParams.data; | ||||||
| 
 | 
 | ||||||
|         const parsedQuery = querySchema.safeParse(req.query); |         const parsedQuery = querySchema.safeParse(req.query); | ||||||
|  | @ -111,7 +109,7 @@ export async function listIdpOrgPolicies( | ||||||
|             }, |             }, | ||||||
|             success: true, |             success: true, | ||||||
|             error: false, |             error: false, | ||||||
|             message: "Idp org policies retrieved successfully", |             message: "Policies retrieved successfully", | ||||||
|             status: HttpCode.OK |             status: HttpCode.OK | ||||||
|         }); |         }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |  | ||||||
|  | @ -1,27 +1,21 @@ | ||||||
| import { Request, Response } from "express"; | // This file is licensed under the Fossorial Commercial License.
 | ||||||
| import { z } from "zod"; | // Unauthorized use, copying, modification, or distribution is strictly prohibited.
 | ||||||
|  | //
 | ||||||
|  | // Copyright (c) 2025 Fossorial LLC. All rights reserved.
 | ||||||
|  | 
 | ||||||
| import { | import { | ||||||
|     createSession, |     createSession, | ||||||
|     generateId, |     generateId, | ||||||
|     generateSessionToken, |     generateSessionToken, | ||||||
|     serializeSessionCookie |     serializeSessionCookie | ||||||
| } from "@server/auth/sessions/app"; | } from "@server/auth/sessions/app"; | ||||||
| import logger from "@server/logger"; |  | ||||||
| import db from "@server/db"; | import db from "@server/db"; | ||||||
| import { | import { Idp, idpOrg, orgs, roles, User, userOrgs, users } from "@server/db/schemas"; | ||||||
|     Idp, | import logger from "@server/logger"; | ||||||
|     idpOrg, | import { UserType } from "@server/types/UserTypes"; | ||||||
|     orgs, |  | ||||||
|     roles, |  | ||||||
|     User, |  | ||||||
|     userOrgs, |  | ||||||
|     users |  | ||||||
| } from "@server/db/schemas"; |  | ||||||
| import { eq, and, inArray } from "drizzle-orm"; | import { eq, and, inArray } from "drizzle-orm"; | ||||||
| import jmespath from "jmespath"; | import jmespath from "jmespath"; | ||||||
| import { UserType } from "@server/types/UserTypes"; | import { Request, Response } from "express"; | ||||||
| 
 |  | ||||||
| const extractedRolesSchema = z.array(z.string()).or(z.string()).nullable(); |  | ||||||
| 
 | 
 | ||||||
| export async function oidcAutoProvision({ | export async function oidcAutoProvision({ | ||||||
|     idp, |     idp, | ||||||
|  | @ -42,92 +36,83 @@ export async function oidcAutoProvision({ | ||||||
|     req: Request; |     req: Request; | ||||||
|     res: Response; |     res: Response; | ||||||
| }) { | }) { | ||||||
|     // Get user's roles of all orgs as stated in the ID token claims
 |  | ||||||
|     const allOrgs = await db.select().from(orgs); |     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) { |     for (const org of allOrgs) { | ||||||
|         const idpOrgs = await db |         const [idpOrgRes] = await db | ||||||
|             .select() |             .select() | ||||||
|             .from(idpOrg) |             .from(idpOrg) | ||||||
|             .where( |             .where( | ||||||
|                 and(eq(idpOrg.idpId, idp.idpId), eq(idpOrg.orgId, org.orgId)) |                 and(eq(idpOrg.idpId, idp.idpId), eq(idpOrg.orgId, org.orgId)) | ||||||
|             ); |             ); | ||||||
|         if (idpOrgs.length === 0) continue; |  | ||||||
|         const idpOrgRes = idpOrgs[0]; |  | ||||||
| 
 | 
 | ||||||
|         const orgMapping = hydrateOrgMapping( |         let roleId: number | undefined = undefined; | ||||||
|             idpOrgRes.orgMapping || idp.defaultOrgMapping, |  | ||||||
|             org.orgId |  | ||||||
|         ); |  | ||||||
|         const roleMapping = idpOrgRes.roleMapping || idp.defaultRoleMapping; |  | ||||||
| 
 | 
 | ||||||
|         if (orgMapping) { |         const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping; | ||||||
|             const orgId = jmespath.search(claims, orgMapping); |         const hydratedOrgMapping = hydrateOrgMapping(orgMapping, org.orgId); | ||||||
|             logger.debug("Extracted org ID", { orgId }); | 
 | ||||||
|  |         if (hydratedOrgMapping) { | ||||||
|  |             logger.debug("Hydrated Org Mapping", { | ||||||
|  |                 hydratedOrgMapping | ||||||
|  |             }); | ||||||
|  |             const orgId = jmespath.search(claims, hydratedOrgMapping); | ||||||
|  |             logger.debug("Extraced Org ID", { orgId }); | ||||||
|             if (orgId !== true && orgId !== org.orgId) { |             if (orgId !== true && orgId !== org.orgId) { | ||||||
|                 // user not allowed to access this org
 |                 // user not allowed to access this org
 | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         const roleMapping = idpOrgRes?.roleMapping || defaultRoleMapping; | ||||||
|         if (roleMapping) { |         if (roleMapping) { | ||||||
|             logger.info("claims", { claims }); |             logger.debug("Role Mapping", { roleMapping }); | ||||||
|             const extractedRoles = extractedRolesSchema.safeParse( |             const roleName = jmespath.search(claims, roleMapping); | ||||||
|                 jmespath.search(claims, roleMapping) | 
 | ||||||
|             ); |             if (!roleName) { | ||||||
|             if (!extractedRoles.success) { |                 logger.error("Role name not found in the ID token", { | ||||||
|                 logger.error("Error extracting roles", { |                     roleName | ||||||
|                     error: extractedRoles.error |  | ||||||
|                 }); |                 }); | ||||||
|                 continue; |                 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 rolesFromDb = await db |             const [roleRes] = await db | ||||||
|                 .select() |                 .select() | ||||||
|                 .from(roles) |                 .from(roles) | ||||||
|                 .where( |                 .where( | ||||||
|                     and( |                     and(eq(roles.orgId, org.orgId), eq(roles.name, roleName)) | ||||||
|                         eq(roles.orgId, org.orgId), |  | ||||||
|                         inArray(roles.name, rolesFromToken) |  | ||||||
|                     ) |  | ||||||
|                 ); |                 ); | ||||||
|             if (rolesFromDb.length === 0) { | 
 | ||||||
|                 logger.error("Role(s) not found", { roles: rolesFromToken }); |             if (!roleRes) { | ||||||
|  |                 logger.error("Role not found", { | ||||||
|  |                     orgId: org.orgId, | ||||||
|  |                     roleName | ||||||
|  |                 }); | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|             if (rolesFromDb.length < rolesFromToken.length) { |  | ||||||
|                 logger.warn("Role(s) not found", { |  | ||||||
|                     roles: rolesFromToken.filter( |  | ||||||
|                         (r) => !rolesFromDb.some((rdb) => rdb.name === r) |  | ||||||
|                     ) |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             rolesFromDb.forEach((r) => { |             roleId = roleRes.roleId; | ||||||
|  | 
 | ||||||
|             userOrgInfo.push({ |             userOrgInfo.push({ | ||||||
|                 orgId: org.orgId, |                 orgId: org.orgId, | ||||||
|                     roleId: r.roleId |                 roleId | ||||||
|                 }); |  | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     logger.debug("User org info", { userOrgInfo }); |     logger.debug("User org info", { userOrgInfo }); | ||||||
| 
 | 
 | ||||||
|     let userId = existingUser?.userId; |     let existingUserId = existingUser?.userId; | ||||||
|  | 
 | ||||||
|     // sync the user with the orgs and roles
 |     // sync the user with the orgs and roles
 | ||||||
|     await db.transaction(async (trx) => { |     await db.transaction(async (trx) => { | ||||||
|         if (!userId) { |         let userId = existingUser?.userId; | ||||||
|             // create user if it does not exist
 | 
 | ||||||
|  |         // create user if not exists
 | ||||||
|  |         if (!existingUser) { | ||||||
|             userId = generateId(15); |             userId = generateId(15); | ||||||
| 
 | 
 | ||||||
|             await trx.insert(users).values({ |             await trx.insert(users).values({ | ||||||
|  | @ -141,7 +126,7 @@ export async function oidcAutoProvision({ | ||||||
|                 dateCreated: new Date().toISOString() |                 dateCreated: new Date().toISOString() | ||||||
|             }); |             }); | ||||||
|         } else { |         } else { | ||||||
|             // update username/email
 |             // set the name and email
 | ||||||
|             await trx |             await trx | ||||||
|                 .update(users) |                 .update(users) | ||||||
|                 .set({ |                 .set({ | ||||||
|  | @ -149,60 +134,84 @@ export async function oidcAutoProvision({ | ||||||
|                     email: email || null, |                     email: email || null, | ||||||
|                     name: name || null |                     name: name || null | ||||||
|                 }) |                 }) | ||||||
|                 .where(eq(users.userId, userId)); |                 .where(eq(users.userId, userId!)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // get all current user orgs/roles
 |         existingUserId = userId; | ||||||
|  | 
 | ||||||
|  |         // get all current user orgs
 | ||||||
|         const currentUserOrgs = await trx |         const currentUserOrgs = await trx | ||||||
|             .select() |             .select() | ||||||
|             .from(userOrgs) |             .from(userOrgs) | ||||||
|             .where(eq(userOrgs.userId, userId)); |             .where(eq(userOrgs.userId, userId!)); | ||||||
| 
 | 
 | ||||||
|         // Delete orgs that are no longer valid
 |         // Delete orgs that are no longer valid
 | ||||||
|         const orgsToDelete = currentUserOrgs |         const orgsToDelete = currentUserOrgs.filter( | ||||||
|             .filter( |  | ||||||
|             (currentOrg) => |             (currentOrg) => | ||||||
|                     !userOrgInfo.some( |                 !userOrgInfo.some((newOrg) => newOrg.orgId === currentOrg.orgId) | ||||||
|                         (newOrg) => |         ); | ||||||
|                             newOrg.orgId === currentOrg.orgId && |  | ||||||
|                             newOrg.roleId === currentOrg.roleId |  | ||||||
|                     ) |  | ||||||
|             ) |  | ||||||
|             .map((org) => org.orgId); |  | ||||||
| 
 | 
 | ||||||
|         if (orgsToDelete.length > 0) { |         if (orgsToDelete.length > 0) { | ||||||
|  |             await trx.delete(userOrgs).where( | ||||||
|  |                 and( | ||||||
|  |                     eq(userOrgs.userId, userId!), | ||||||
|  |                     inArray( | ||||||
|  |                         userOrgs.orgId, | ||||||
|  |                         orgsToDelete.map((org) => org.orgId) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 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 |                     await trx | ||||||
|                 .delete(userOrgs) |                         .update(userOrgs) | ||||||
|  |                         .set({ roleId: newRole.roleId }) | ||||||
|                         .where( |                         .where( | ||||||
|                             and( |                             and( | ||||||
|                                 eq(userOrgs.userId, userId!), |                                 eq(userOrgs.userId, userId!), | ||||||
|                         inArray(userOrgs.orgId, orgsToDelete) |                                 eq(userOrgs.orgId, org.orgId) | ||||||
|                             ) |                             ) | ||||||
|                         ); |                         ); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // Add new orgs that don't exist yet
 |         // Add new orgs that don't exist yet
 | ||||||
|         const orgsToAdd = userOrgInfo.filter( |         const orgsToAdd = userOrgInfo.filter( | ||||||
|             (newOrg) => |             (newOrg) => | ||||||
|                 !currentUserOrgs.some( |                 !currentUserOrgs.some( | ||||||
|                     (currentOrg) => |                     (currentOrg) => currentOrg.orgId === newOrg.orgId | ||||||
|                         currentOrg.orgId === newOrg.orgId && |  | ||||||
|                         currentOrg.roleId === newOrg.roleId |  | ||||||
|                 ) |                 ) | ||||||
|         ); |         ); | ||||||
|  | 
 | ||||||
|         if (orgsToAdd.length > 0) { |         if (orgsToAdd.length > 0) { | ||||||
|             await trx.insert(userOrgs).values( |             await trx.insert(userOrgs).values( | ||||||
|                 orgsToAdd.map((org) => ({ |                 orgsToAdd.map((org) => ({ | ||||||
|                     userId: userId!, |                     userId: userId!, | ||||||
|                     orgId: org.orgId, |                     orgId: org.orgId, | ||||||
|                     roleId: org.roleId |                     roleId: org.roleId, | ||||||
|  |                     dateCreated: new Date().toISOString() | ||||||
|                 })) |                 })) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const token = generateSessionToken(); |     const token = generateSessionToken(); | ||||||
|     const sess = await createSession(token, userId!); |     const sess = await createSession(token, existingUserId!); | ||||||
|     const isSecure = req.protocol === "https"; |     const isSecure = req.protocol === "https"; | ||||||
|     const cookie = serializeSessionCookie( |     const cookie = serializeSessionCookie( | ||||||
|         token, |         token, | ||||||
|  | @ -216,9 +225,9 @@ export async function oidcAutoProvision({ | ||||||
| function hydrateOrgMapping( | function hydrateOrgMapping( | ||||||
|     orgMapping: string | null, |     orgMapping: string | null, | ||||||
|     orgId: string |     orgId: string | ||||||
| ): string | null { | ): string | undefined { | ||||||
|     if (!orgMapping) { |     if (!orgMapping) { | ||||||
|         return null; |         return undefined; | ||||||
|     } |     } | ||||||
|     return orgMapping.replaceAll("{{orgId}}", orgId); |     return orgMapping.split("{{orgId}}").join(orgId); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | // 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 { Request, Response, NextFunction } from "express"; | ||||||
| import { z } from "zod"; | import { z } from "zod"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
|  | @ -7,8 +12,8 @@ import createHttpError from "http-errors"; | ||||||
| import logger from "@server/logger"; | import logger from "@server/logger"; | ||||||
| import { fromError } from "zod-validation-error"; | import { fromError } from "zod-validation-error"; | ||||||
| import { OpenAPITags, registry } from "@server/openApi"; | import { OpenAPITags, registry } from "@server/openApi"; | ||||||
| import { idp, idpOrg } from "@server/db/schemas"; |  | ||||||
| import { eq, and } from "drizzle-orm"; | import { eq, and } from "drizzle-orm"; | ||||||
|  | import { idp, idpOrg } from "@server/db/schemas"; | ||||||
| 
 | 
 | ||||||
| const paramsSchema = z | const paramsSchema = z | ||||||
|     .object({ |     .object({ | ||||||
|  | @ -59,7 +64,6 @@ export async function updateIdpOrgPolicy( | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         const { idpId, orgId } = parsedParams.data; |  | ||||||
| 
 | 
 | ||||||
|         const parsedBody = bodySchema.safeParse(req.body); |         const parsedBody = bodySchema.safeParse(req.body); | ||||||
|         if (!parsedBody.success) { |         if (!parsedBody.success) { | ||||||
|  | @ -70,9 +74,11 @@ export async function updateIdpOrgPolicy( | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         let { orgMapping, roleMapping } = parsedBody.data; |  | ||||||
| 
 | 
 | ||||||
|         // Given identity provider must exist and have a policy already
 |         const { idpId, orgId } = parsedParams.data; | ||||||
|  |         const { roleMapping, orgMapping } = parsedBody.data; | ||||||
|  | 
 | ||||||
|  |         // Check if IDP and policy exist
 | ||||||
|         const [existing] = await db |         const [existing] = await db | ||||||
|             .select() |             .select() | ||||||
|             .from(idp) |             .from(idp) | ||||||
|  | @ -84,36 +90,36 @@ export async function updateIdpOrgPolicy( | ||||||
| 
 | 
 | ||||||
|         if (!existing?.idp) { |         if (!existing?.idp) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError(HttpCode.NOT_FOUND, "Idp does not exist") |                 createHttpError( | ||||||
|  |                     HttpCode.BAD_REQUEST, | ||||||
|  |                     "An IDP with this ID does not exist." | ||||||
|  |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!existing.idpOrg) { |         if (!existing.idpOrg) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError( |                 createHttpError( | ||||||
|                     HttpCode.NOT_FOUND, |                     HttpCode.BAD_REQUEST, | ||||||
|                     "Org policy does not exist for this idp" |                     "A policy for this IDP and org does not exist." | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // Update the policy
 | ||||||
|         await db |         await db | ||||||
|             .update(idpOrg) |             .update(idpOrg) | ||||||
|             .set({ |             .set({ | ||||||
|                 idpId, |                 roleMapping, | ||||||
|                 orgId, |                 orgMapping | ||||||
|                 orgMapping, |  | ||||||
|                 roleMapping |  | ||||||
|             }) |             }) | ||||||
|             .where(and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId))); |             .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); | ||||||
| 
 | 
 | ||||||
|         return response<UpdateIdpOrgPolicyResponse>(res, { |         return response<UpdateIdpOrgPolicyResponse>(res, { | ||||||
|             data: { |             data: {}, | ||||||
|                 idpId |  | ||||||
|             }, |  | ||||||
|             success: true, |             success: true, | ||||||
|             error: false, |             error: false, | ||||||
|             message: "Idp org policy updated successfully", |             message: "Policy updated successfully", | ||||||
|             status: HttpCode.OK |             status: HttpCode.OK | ||||||
|         }); |         }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import { idp, idpOidcConfig } from "@server/db/schemas"; | ||||||
| import { eq } from "drizzle-orm"; | import { eq } from "drizzle-orm"; | ||||||
| import { encrypt } from "@server/lib/crypto"; | import { encrypt } from "@server/lib/crypto"; | ||||||
| import config from "@server/lib/config"; | import config from "@server/lib/config"; | ||||||
|  | import license from "@server/license/license"; | ||||||
| 
 | 
 | ||||||
| const paramsSchema = z | const paramsSchema = z | ||||||
|     .object({ |     .object({ | ||||||
|  | @ -99,6 +100,10 @@ export async function updateOidcIdp( | ||||||
|             defaultOrgMapping |             defaultOrgMapping | ||||||
|         } = parsedBody.data; |         } = parsedBody.data; | ||||||
| 
 | 
 | ||||||
|  |         if (!(await license.isUnlocked())) { | ||||||
|  |             autoProvision = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Check if IDP exists and is of type OIDC
 |         // Check if IDP exists and is of type OIDC
 | ||||||
|         const [existingIdp] = await db |         const [existingIdp] = await db | ||||||
|             .select() |             .select() | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ import { | ||||||
| } from "@server/auth/sessions/app"; | } from "@server/auth/sessions/app"; | ||||||
| import { decrypt } from "@server/lib/crypto"; | import { decrypt } from "@server/lib/crypto"; | ||||||
| import { oidcAutoProvision } from "./oidcAutoProvision"; | import { oidcAutoProvision } from "./oidcAutoProvision"; | ||||||
|  | import license from "@server/license/license"; | ||||||
| 
 | 
 | ||||||
| const ensureTrailingSlash = (url: string): string => { | const ensureTrailingSlash = (url: string): string => { | ||||||
|     return url; |     return url; | ||||||
|  | @ -209,6 +210,14 @@ export async function validateOidcCallback( | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|         if (existingIdp.idp.autoProvision) { |         if (existingIdp.idp.autoProvision) { | ||||||
|  |             if (!(await license.isUnlocked())) { | ||||||
|  |                 return next( | ||||||
|  |                     createHttpError( | ||||||
|  |                         HttpCode.FORBIDDEN, | ||||||
|  |                         "Auto-provisioning is not available" | ||||||
|  |                     ) | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|             await oidcAutoProvision({ |             await oidcAutoProvision({ | ||||||
|                 idp: existingIdp.idp, |                 idp: existingIdp.idp, | ||||||
|                 userIdentifier, |                 userIdentifier, | ||||||
|  |  | ||||||
							
								
								
									
										499
									
								
								server/routers/integration.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										499
									
								
								server/routers/integration.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,499 @@ | ||||||
|  | // 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,6 +4,8 @@ import * as traefik from "@server/routers/traefik"; | ||||||
| import * as resource from "./resource"; | import * as resource from "./resource"; | ||||||
| import * as badger from "./badger"; | import * as badger from "./badger"; | ||||||
| import * as auth from "@server/routers/auth"; | 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 HttpCode from "@server/types/HttpCode"; | ||||||
| import { | import { | ||||||
|     verifyResourceAccess, |     verifyResourceAccess, | ||||||
|  | @ -31,6 +33,16 @@ internalRouter.post( | ||||||
|     resource.getExchangeToken |     resource.getExchangeToken | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | internalRouter.get( | ||||||
|  |     `/supporter-key/visible`, | ||||||
|  |     supporterKey.isSupporterKeyVisible | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | internalRouter.get( | ||||||
|  |     `/license/status`, | ||||||
|  |     license.getLicenseStatus | ||||||
|  | ); | ||||||
|  | 
 | ||||||
| // Gerbil routes
 | // Gerbil routes
 | ||||||
| const gerbilRouter = Router(); | const gerbilRouter = Router(); | ||||||
| internalRouter.use("/gerbil", gerbilRouter); | internalRouter.use("/gerbil", gerbilRouter); | ||||||
|  |  | ||||||
							
								
								
									
										62
									
								
								server/routers/license/activateLicense.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								server/routers/license/activateLicense.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								server/routers/license/deleteLicenseKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								server/routers/license/deleteLicenseKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								server/routers/license/getLicenseStatus.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/routers/license/getLicenseStatus.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								server/routers/license/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/routers/license/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | // 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"; | ||||||
							
								
								
									
										36
									
								
								server/routers/license/listLicenseKeys.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/routers/license/listLicenseKeys.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | // 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								server/routers/license/recheckStatus.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/routers/license/recheckStatus.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | // 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; |         const { newtId, secret } = parsedBody.data; | ||||||
| 
 | 
 | ||||||
|         if (req.user && !req.userRoleIds) { |         if (req.user && !req.userOrgRoleId) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError(HttpCode.FORBIDDEN, "User does not have a role") |                 createHttpError(HttpCode.FORBIDDEN, "User does not have a role") | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
|  | @ -11,13 +11,12 @@ import { | ||||||
|     users, |     users, | ||||||
|     userSites |     userSites | ||||||
| } from "@server/db/schemas"; | } from "@server/db/schemas"; | ||||||
| import { and, count, eq, inArray, countDistinct } from "drizzle-orm"; | import { and, count, eq, inArray } from "drizzle-orm"; | ||||||
| import response from "@server/lib/response"; | import response from "@server/lib/response"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import logger from "@server/logger"; | import logger from "@server/logger"; | ||||||
| import { fromZodError } from "zod-validation-error"; | import { fromZodError } from "zod-validation-error"; | ||||||
| import { RoleItem } from "../user/getOrgUser"; |  | ||||||
| 
 | 
 | ||||||
| const getOrgParamsSchema = z | const getOrgParamsSchema = z | ||||||
|     .object({ |     .object({ | ||||||
|  | @ -28,7 +27,7 @@ const getOrgParamsSchema = z | ||||||
| export type GetOrgOverviewResponse = { | export type GetOrgOverviewResponse = { | ||||||
|     orgName: string; |     orgName: string; | ||||||
|     orgId: string; |     orgId: string; | ||||||
|     roles: RoleItem[]; |     userRoleName: string; | ||||||
|     numSites: number; |     numSites: number; | ||||||
|     numUsers: number; |     numUsers: number; | ||||||
|     numResources: number; |     numResources: number; | ||||||
|  | @ -116,25 +115,24 @@ export async function getOrgOverview( | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|         const [{ numUsers }] = await db |         const [{ numUsers }] = await db | ||||||
|             .select({ numUsers: countDistinct(userOrgs.userId) }) |             .select({ numUsers: count() }) | ||||||
|             .from(userOrgs) |             .from(userOrgs) | ||||||
|             .where(eq(userOrgs.orgId, orgId)); |             .where(eq(userOrgs.orgId, orgId)); | ||||||
| 
 | 
 | ||||||
|         const userRoles = await db |         const [role] = await db | ||||||
|             .select({ id: roles.roleId, name: roles.name }) |             .select() | ||||||
|             .from(roles) |             .from(roles) | ||||||
|             .where(inArray(roles.roleId, req.userRoleIds ?? [])) |             .where(eq(roles.roleId, req.userOrg.roleId)); | ||||||
|             .orderBy(roles.name); |  | ||||||
| 
 | 
 | ||||||
|         return response<GetOrgOverviewResponse>(res, { |         return response<GetOrgOverviewResponse>(res, { | ||||||
|             data: { |             data: { | ||||||
|                 orgName: org[0].name, |                 orgName: org[0].name, | ||||||
|                 orgId: org[0].orgId, |                 orgId: org[0].orgId, | ||||||
|                 roles: userRoles, |                 userRoleName: role.name, | ||||||
|                 numSites, |                 numSites, | ||||||
|                 numUsers, |                 numUsers, | ||||||
|                 numResources, |                 numResources, | ||||||
|                 isAdmin: userRoles.some((r) => r.name === "Admin"), |                 isAdmin: role.name === "Admin", | ||||||
|                 isOwner: req.userOrg?.isOwner || false |                 isOwner: req.userOrg?.isOwner || false | ||||||
|             }, |             }, | ||||||
|             success: true, |             success: true, | ||||||
|  |  | ||||||
|  | @ -130,7 +130,7 @@ export async function createResource( | ||||||
| 
 | 
 | ||||||
|         const { siteId, orgId } = parsedParams.data; |         const { siteId, orgId } = parsedParams.data; | ||||||
| 
 | 
 | ||||||
|         if (req.user && !req.userRoleIds) { |         if (req.user && !req.userOrgRoleId) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError(HttpCode.FORBIDDEN, "User does not have a role") |                 createHttpError(HttpCode.FORBIDDEN, "User does not have a role") | ||||||
|             ); |             ); | ||||||
|  | @ -285,7 +285,7 @@ async function createHttpResource( | ||||||
|             resourceId: newResource[0].resourceId |             resourceId: newResource[0].resourceId | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { |         if (req.user && req.userOrgRoleId != adminRole[0].roleId) { | ||||||
|             // make sure the user can access the resource
 |             // make sure the user can access the resource
 | ||||||
|             await trx.insert(userResources).values({ |             await trx.insert(userResources).values({ | ||||||
|                 userId: req.user?.userId!, |                 userId: req.user?.userId!, | ||||||
|  | @ -392,7 +392,7 @@ async function createRawResource( | ||||||
|             resourceId: newResource[0].resourceId |             resourceId: newResource[0].resourceId | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { |         if (req.userOrgRoleId != adminRole[0].roleId) { | ||||||
|             // make sure the user can access the resource
 |             // make sure the user can access the resource
 | ||||||
|             await trx.insert(userResources).values({ |             await trx.insert(userResources).values({ | ||||||
|                 userId: req.user?.userId!, |                 userId: req.user?.userId!, | ||||||
|  |  | ||||||
|  | @ -216,7 +216,7 @@ export async function listResources( | ||||||
|             .where( |             .where( | ||||||
|                 or( |                 or( | ||||||
|                     eq(userResources.userId, req.user!.userId), |                     eq(userResources.userId, req.user!.userId), | ||||||
|                     inArray(roleResources.roleId, req.userRoleIds!) |                     eq(roleResources.roleId, req.userOrgRoleId!) | ||||||
|                 ) |                 ) | ||||||
|             ); |             ); | ||||||
|         } else { |         } else { | ||||||
|  |  | ||||||
|  | @ -99,7 +99,7 @@ export async function createSite( | ||||||
| 
 | 
 | ||||||
|         const { orgId } = parsedParams.data; |         const { orgId } = parsedParams.data; | ||||||
| 
 | 
 | ||||||
|         if (req.user && !req.userRoleIds) { |         if (req.user && !req.userOrgRoleId) { | ||||||
|             return next( |             return next( | ||||||
|                 createHttpError(HttpCode.FORBIDDEN, "User does not have a role") |                 createHttpError(HttpCode.FORBIDDEN, "User does not have a role") | ||||||
|             ); |             ); | ||||||
|  | @ -176,7 +176,7 @@ export async function createSite( | ||||||
|                 siteId: newSite.siteId |                 siteId: newSite.siteId | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { |             if (req.user && req.userOrgRoleId != adminRole[0].roleId) { | ||||||
|                 // make sure the user can access the site
 |                 // make sure the user can access the site
 | ||||||
|                 trx.insert(userSites).values({ |                 trx.insert(userSites).values({ | ||||||
|                     userId: req.user?.userId!, |                     userId: req.user?.userId!, | ||||||
|  |  | ||||||
|  | @ -120,7 +120,7 @@ export async function listSites( | ||||||
|                 .where( |                 .where( | ||||||
|                     or( |                     or( | ||||||
|                         eq(userSites.userId, req.user!.userId), |                         eq(userSites.userId, req.user!.userId), | ||||||
|                         inArray(roleSites.roleId, req.userRoleIds!) |                         eq(roleSites.roleId, req.userOrgRoleId!) | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ); | ||||||
|         } else { |         } else { | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								server/routers/supporterKey/hideSupporterKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								server/routers/supporterKey/hideSupporterKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								server/routers/supporterKey/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								server/routers/supporterKey/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export * from "./validateSupporterKey"; | ||||||
|  | export * from "./isSupporterKeyVisible"; | ||||||
|  | export * from "./hideSupporterKey"; | ||||||
							
								
								
									
										63
									
								
								server/routers/supporterKey/isSupporterKeyVisible.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								server/routers/supporterKey/isSupporterKeyVisible.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | 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") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										115
									
								
								server/routers/supporterKey/validateSupporterKey.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								server/routers/supporterKey/validateSupporterKey.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | ||||||
|  | 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,26 +105,14 @@ export async function addUserRole( | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const newUserRole = { orgId, userId, roleId, isOwner: false }; |         const newUserRole = await db | ||||||
| 
 |             .update(userOrgs) | ||||||
|         await db.transaction(async (trx) => { |             .set({ roleId }) | ||||||
|             const hasRoleAlready = await trx |             .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) | ||||||
|                 .select() |             .returning(); | ||||||
|                 .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, { |         return response(res, { | ||||||
|             data: newUserRole, |             data: newUserRole[0], | ||||||
|             success: true, |             success: true, | ||||||
|             error: false, |             error: false, | ||||||
|             message: "Role added to user successfully", |             message: "Role added to user successfully", | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express"; | ||||||
| import { z } from "zod"; | import { z } from "zod"; | ||||||
| import { db } from "@server/db"; | import { db } from "@server/db"; | ||||||
| import { roles, userOrgs, users } from "@server/db/schemas"; | import { roles, userOrgs, users } from "@server/db/schemas"; | ||||||
| import { and, eq, sql } from "drizzle-orm"; | import { and, eq } from "drizzle-orm"; | ||||||
| import response from "@server/lib/response"; | import response from "@server/lib/response"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
|  | @ -10,7 +10,6 @@ import logger from "@server/logger"; | ||||||
| import { fromError } from "zod-validation-error"; | import { fromError } from "zod-validation-error"; | ||||||
| import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; | import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; | ||||||
| import { OpenAPITags, registry } from "@server/openApi"; | import { OpenAPITags, registry } from "@server/openApi"; | ||||||
| import { StringDecoder } from "string_decoder"; |  | ||||||
| 
 | 
 | ||||||
| async function queryUser(orgId: string, userId: string) { | async function queryUser(orgId: string, userId: string) { | ||||||
|     const [user] = await db |     const [user] = await db | ||||||
|  | @ -21,7 +20,8 @@ async function queryUser(orgId: string, userId: string) { | ||||||
|             username: users.username, |             username: users.username, | ||||||
|             name: users.name, |             name: users.name, | ||||||
|             type: users.type, |             type: users.type, | ||||||
|             roles: sql<RoleItem[]>`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`, |             roleId: userOrgs.roleId, | ||||||
|  |             roleName: roles.name, | ||||||
|             isOwner: userOrgs.isOwner, |             isOwner: userOrgs.isOwner, | ||||||
|             isAdmin: roles.isAdmin |             isAdmin: roles.isAdmin | ||||||
|         }) |         }) | ||||||
|  | @ -30,17 +30,9 @@ async function queryUser(orgId: string, userId: string) { | ||||||
|         .leftJoin(users, eq(userOrgs.userId, users.userId)) |         .leftJoin(users, eq(userOrgs.userId, users.userId)) | ||||||
|         .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) |         .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) | ||||||
|         .limit(1); |         .limit(1); | ||||||
|     if (typeof user.roles === "string") { |  | ||||||
|         user.roles = JSON.parse(user.roles); |  | ||||||
|     } |  | ||||||
|     return user; |     return user; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type RoleItem = { |  | ||||||
|     id: number; |  | ||||||
|     name: string; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export type GetOrgUserResponse = NonNullable< | export type GetOrgUserResponse = NonNullable< | ||||||
|     Awaited<ReturnType<typeof queryUser>> |     Awaited<ReturnType<typeof queryUser>> | ||||||
| >; | >; | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ export * from "./getUser"; | ||||||
| export * from "./removeUserOrg"; | export * from "./removeUserOrg"; | ||||||
| export * from "./listUsers"; | export * from "./listUsers"; | ||||||
| export * from "./addUserRole"; | export * from "./addUserRole"; | ||||||
| export * from "./setUserRoles"; |  | ||||||
| export * from "./inviteUser"; | export * from "./inviteUser"; | ||||||
| export * from "./acceptInvite"; | export * from "./acceptInvite"; | ||||||
| export * from "./getOrgUser"; | export * from "./getOrgUser"; | ||||||
|  |  | ||||||
|  | @ -5,11 +5,11 @@ import { idp, roles, userOrgs, users } from "@server/db/schemas"; | ||||||
| import response from "@server/lib/response"; | import response from "@server/lib/response"; | ||||||
| import HttpCode from "@server/types/HttpCode"; | import HttpCode from "@server/types/HttpCode"; | ||||||
| import createHttpError from "http-errors"; | import createHttpError from "http-errors"; | ||||||
| import { AnyColumn, eq, InferColumnsDataTypes, sql } from "drizzle-orm"; | import { and, sql } from "drizzle-orm"; | ||||||
| import logger from "@server/logger"; | import logger from "@server/logger"; | ||||||
| import { fromZodError } from "zod-validation-error"; | import { fromZodError } from "zod-validation-error"; | ||||||
| import { OpenAPITags, registry } from "@server/openApi"; | import { OpenAPITags, registry } from "@server/openApi"; | ||||||
| import { RoleItem } from "./getOrgUser"; | import { eq } from "drizzle-orm"; | ||||||
| 
 | 
 | ||||||
| const listUsersParamsSchema = z | const listUsersParamsSchema = z | ||||||
|     .object({ |     .object({ | ||||||
|  | @ -34,20 +34,8 @@ const listUsersSchema = z | ||||||
|     }) |     }) | ||||||
|     .strict(); |     .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) { | async function queryUsers(orgId: string, limit: number, offset: number) { | ||||||
|     const res = await db |     return await db | ||||||
|         .select({ |         .select({ | ||||||
|             id: users.userId, |             id: users.userId, | ||||||
|             email: users.email, |             email: users.email, | ||||||
|  | @ -57,7 +45,8 @@ async function queryUsers(orgId: string, limit: number, offset: number) { | ||||||
|             username: users.username, |             username: users.username, | ||||||
|             name: users.name, |             name: users.name, | ||||||
|             type: users.type, |             type: users.type, | ||||||
|             roles: sql<RoleItem[]>`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`, |             roleId: userOrgs.roleId, | ||||||
|  |             roleName: roles.name, | ||||||
|             isOwner: userOrgs.isOwner, |             isOwner: userOrgs.isOwner, | ||||||
|             idpName: idp.name, |             idpName: idp.name, | ||||||
|             idpId: users.idpId |             idpId: users.idpId | ||||||
|  | @ -67,15 +56,8 @@ async function queryUsers(orgId: string, limit: number, offset: number) { | ||||||
|         .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) |         .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) | ||||||
|         .leftJoin(idp, eq(users.idpId, idp.idpId)) |         .leftJoin(idp, eq(users.idpId, idp.idpId)) | ||||||
|         .where(eq(userOrgs.orgId, orgId)) |         .where(eq(userOrgs.orgId, orgId)) | ||||||
|         .groupBy(users.userId) |  | ||||||
|         .limit(limit) |         .limit(limit) | ||||||
|         .offset(offset); |         .offset(offset); | ||||||
|     res.forEach((itm) => { |  | ||||||
|         if (typeof itm.roles === "string") { |  | ||||||
|             itm.roles = JSON.parse(itm.roles); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|     return res; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type ListUsersResponse = { | export type ListUsersResponse = { | ||||||
|  |  | ||||||
|  | @ -1,175 +0,0 @@ | ||||||
| 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,8 +10,7 @@ import { | ||||||
|     CardFooter |     CardFooter | ||||||
| } from "@/components/ui/card"; | } from "@/components/ui/card"; | ||||||
| import { Button } from "@/components/ui/button"; | import { Button } from "@/components/ui/button"; | ||||||
| import { Users, Settings, Waypoints, Combine } from "lucide-react"; | import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react"; | ||||||
| import { RoleItem } from "@server/routers/user"; |  | ||||||
| 
 | 
 | ||||||
| interface OrgStat { | interface OrgStat { | ||||||
|     label: string; |     label: string; | ||||||
|  | @ -27,7 +26,7 @@ type OrganizationLandingCardProps = { | ||||||
|             resources: number; |             resources: number; | ||||||
|             users: number; |             users: number; | ||||||
|         }; |         }; | ||||||
|         roles: RoleItem[]; |         userRole: string; | ||||||
|         isAdmin: boolean; |         isAdmin: boolean; | ||||||
|         isOwner: boolean; |         isOwner: boolean; | ||||||
|         orgId: string; |         orgId: string; | ||||||
|  | @ -82,21 +81,9 @@ export default function OrganizationLandingCard( | ||||||
|                     ))} |                     ))} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div className="text-center text-lg"> |                 <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"> |                     <span className="font-semibold"> | ||||||
|                         {orgData.overview.isOwner |                         {orgData.overview.isOwner ? "Owner" : orgData.overview.userRole} | ||||||
|                             ? "Owner" |  | ||||||
|                             : orgData.overview.isAdmin |  | ||||||
|                               ? "Admin" |  | ||||||
|                               : orgData.overview.roles |  | ||||||
|                                     .map((r) => r.name) |  | ||||||
|                                     .join(", ")} |  | ||||||
|                     </span> |                     </span> | ||||||
|                 </div> |                 </div> | ||||||
|             </CardContent> |             </CardContent> | ||||||
|  |  | ||||||
|  | @ -74,7 +74,7 @@ export default async function OrgPage(props: OrgPageProps) { | ||||||
|                                 }, |                                 }, | ||||||
|                                 isAdmin: overview.isAdmin, |                                 isAdmin: overview.isAdmin, | ||||||
|                                 isOwner: overview.isOwner, |                                 isOwner: overview.isOwner, | ||||||
|                                 roles: overview.roles |                                 userRole: overview.userRoleName | ||||||
|                             }} |                             }} | ||||||
|                         /> |                         /> | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
|  | @ -150,7 +150,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { | ||||||
|                             column.toggleSorting(column.getIsSorted() === "asc") |                             column.toggleSorting(column.getIsSorted() === "asc") | ||||||
|                         } |                         } | ||||||
|                     > |                     > | ||||||
|                         Roles |                         Role | ||||||
|                         <ArrowUpDown className="ml-2 h-4 w-4" /> |                         <ArrowUpDown className="ml-2 h-4 w-4" /> | ||||||
|                     </Button> |                     </Button> | ||||||
|                 ); |                 ); | ||||||
|  |  | ||||||
|  | @ -8,9 +8,17 @@ import { | ||||||
|     FormLabel, |     FormLabel, | ||||||
|     FormMessage |     FormMessage | ||||||
| } from "@app/components/ui/form"; | } 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 { toast } from "@app/hooks/useToast"; | ||||||
| import { zodResolver } from "@hookform/resolvers/zod"; | import { zodResolver } from "@hookform/resolvers/zod"; | ||||||
| import { SetUserRolesResponse } from "@server/routers/user"; | import { InviteUserResponse } from "@server/routers/user"; | ||||||
| import { AxiosResponse } from "axios"; | import { AxiosResponse } from "axios"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { useForm } from "react-hook-form"; | import { useForm } from "react-hook-form"; | ||||||
|  | @ -32,18 +40,10 @@ import { | ||||||
| import { formatAxiosError } from "@app/lib/api"; | import { formatAxiosError } from "@app/lib/api"; | ||||||
| import { createApiClient } from "@app/lib/api"; | import { createApiClient } from "@app/lib/api"; | ||||||
| import { useEnvContext } from "@app/hooks/useEnvContext"; | import { useEnvContext } from "@app/hooks/useEnvContext"; | ||||||
| import { Tag, TagInput } from "@app/components/tags/tag-input"; |  | ||||||
| 
 | 
 | ||||||
| const formSchema = z.object({ | const formSchema = z.object({ | ||||||
|     email: z.string().email({ message: "Please enter a valid email" }), |     email: z.string().email({ message: "Please enter a valid email" }), | ||||||
|     roles: z |     roleId: z.string().min(1, { message: "Please select a role" }) | ||||||
|         .array( |  | ||||||
|             z.object({ |  | ||||||
|                 id: z.string(), |  | ||||||
|                 text: z.string() |  | ||||||
|             }) |  | ||||||
|         ) |  | ||||||
|         .min(1, { message: "Please select a role" }) |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export default function AccessControlsPage() { | export default function AccessControlsPage() { | ||||||
|  | @ -54,18 +54,13 @@ export default function AccessControlsPage() { | ||||||
|     const { orgId } = useParams(); |     const { orgId } = useParams(); | ||||||
| 
 | 
 | ||||||
|     const [loading, setLoading] = useState(false); |     const [loading, setLoading] = useState(false); | ||||||
|     const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( |     const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); | ||||||
|         [] |  | ||||||
|     ); |  | ||||||
|     const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< |  | ||||||
|         number | null |  | ||||||
|     >(null); |  | ||||||
| 
 | 
 | ||||||
|     const form = useForm<z.infer<typeof formSchema>>({ |     const form = useForm<z.infer<typeof formSchema>>({ | ||||||
|         resolver: zodResolver(formSchema), |         resolver: zodResolver(formSchema), | ||||||
|         defaultValues: { |         defaultValues: { | ||||||
|             email: user.email!, |             email: user.email!, | ||||||
|             roles: [] |             roleId: user.roleId?.toString() | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -86,24 +81,13 @@ export default function AccessControlsPage() { | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|             if (res?.status === 200) { |             if (res?.status === 200) { | ||||||
|                 setAllRoles( |                 setRoles(res.data.data.roles); | ||||||
|                     res.data.data.roles.map((role) => ({ |  | ||||||
|                         id: role.roleId.toString(), |  | ||||||
|                         text: role.name |  | ||||||
|                     })) |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fetchRoles(); |         fetchRoles(); | ||||||
| 
 | 
 | ||||||
|         form.setValue( |         form.setValue("roleId", user.roleId.toString()); | ||||||
|             "roles", |  | ||||||
|             user.roles.map((i) => ({ |  | ||||||
|                 id: i.id.toString(), |  | ||||||
|                 text: i.name |  | ||||||
|             })) |  | ||||||
|         ); |  | ||||||
|     }, []); |     }, []); | ||||||
| 
 | 
 | ||||||
|     async function onSubmit(values: z.infer<typeof formSchema>) { |     async function onSubmit(values: z.infer<typeof formSchema>) { | ||||||
|  | @ -111,8 +95,8 @@ export default function AccessControlsPage() { | ||||||
| 
 | 
 | ||||||
|         const res = await api |         const res = await api | ||||||
|             .post< |             .post< | ||||||
|                 AxiosResponse<SetUserRolesResponse> |                 AxiosResponse<InviteUserResponse> | ||||||
|             >(`/org/${user.orgId}/user/${user.userId}/roles`, { roleIds: values.roles.map((r) => parseInt(r.id)) }) |             >(`/role/${values.roleId}/add/${user.userId}`) | ||||||
|             .catch((e) => { |             .catch((e) => { | ||||||
|                 toast({ |                 toast({ | ||||||
|                     variant: "destructive", |                     variant: "destructive", | ||||||
|  | @ -156,44 +140,30 @@ export default function AccessControlsPage() { | ||||||
|                             > |                             > | ||||||
|                                 <FormField |                                 <FormField | ||||||
|                                     control={form.control} |                                     control={form.control} | ||||||
|                                     name="roles" |                                     name="roleId" | ||||||
|                                     render={({ field }) => ( |                                     render={({ field }) => ( | ||||||
|                                         <FormItem className="flex flex-col items-start"> |                                         <FormItem> | ||||||
|                                             <FormLabel>Roles</FormLabel> |                                             <FormLabel>Role</FormLabel> | ||||||
|  |                                             <Select | ||||||
|  |                                                 onValueChange={field.onChange} | ||||||
|  |                                                 value={field.value} | ||||||
|  |                                             > | ||||||
|                                                 <FormControl> |                                                 <FormControl> | ||||||
|                                                 <TagInput |                                                     <SelectTrigger> | ||||||
|                                                     {...field} |                                                         <SelectValue placeholder="Select role" /> | ||||||
|                                                     activeTagIndex={ |                                                     </SelectTrigger> | ||||||
|                                                         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> |                                                 </FormControl> | ||||||
|  |                                                 <SelectContent> | ||||||
|  |                                                     {roles.map((role) => ( | ||||||
|  |                                                         <SelectItem | ||||||
|  |                                                             key={role.roleId} | ||||||
|  |                                                             value={role.roleId.toString()} | ||||||
|  |                                                         > | ||||||
|  |                                                             {role.name} | ||||||
|  |                                                         </SelectItem> | ||||||
|  |                                                     ))} | ||||||
|  |                                                 </SelectContent> | ||||||
|  |                                             </Select> | ||||||
|                                             <FormMessage /> |                                             <FormMessage /> | ||||||
|                                         </FormItem> |                                         </FormItem> | ||||||
|                                     )} |                                     )} | ||||||
|  |  | ||||||
|  | @ -78,7 +78,7 @@ export default async function UsersPage(props: UsersPageProps) { | ||||||
|             idpId: user.idpId, |             idpId: user.idpId, | ||||||
|             idpName: user.idpName || "Internal", |             idpName: user.idpName || "Internal", | ||||||
|             status: "Confirmed", |             status: "Confirmed", | ||||||
|             role: user.isOwner ? "Owner" : user.roles.map((r) => r.name).join(", ") || "Member", |             role: user.isOwner ? "Owner" : user.roleName || "Member", | ||||||
|             isOwner: user.isOwner || false |             isOwner: user.isOwner || false | ||||||
|         }; |         }; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | // 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" | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										204
									
								
								src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,204 @@ | ||||||
|  | // 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`); | ||||||
|  |                 }} | ||||||
|  |             /> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | // 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> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | // 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`); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,138 @@ | ||||||
|  | // 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> | ||||||
|  |             )} | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										412
									
								
								src/app/[orgId]/settings/api-keys/create/page.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								src/app/[orgId]/settings/api-keys/create/page.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,412 @@ | ||||||
|  | // 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> | ||||||
|  |             )} | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								src/app/[orgId]/settings/api-keys/page.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/app/[orgId]/settings/api-keys/page.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | ||||||
|  | // 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} /> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								src/app/admin/api-keys/ApiKeysDataTable.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/app/admin/api-keys/ApiKeysDataTable.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | ||||||
|  | // 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" | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										199
									
								
								src/app/admin/api-keys/ApiKeysTable.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								src/app/admin/api-keys/ApiKeysTable.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,199 @@ | ||||||
|  | // 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`); | ||||||
|  |                 }} | ||||||
|  |             /> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								src/app/admin/api-keys/[apiKeyId]/layout.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/app/admin/api-keys/[apiKeyId]/layout.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | // 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> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/app/admin/api-keys/[apiKeyId]/page.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/app/admin/api-keys/[apiKeyId]/page.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | // 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
		Add a link
		
	
		Reference in a new issue