diff --git a/Dockerfile b/Dockerfile index 6ec9e23..6b2c55a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,7 @@ FROM node:20-alpine AS builder WORKDIR /app -# COPY package.json package-lock.json ./ -COPY package.json ./ +COPY package.json package-lock.json ./ RUN npm install COPY . . @@ -21,7 +20,7 @@ RUN apk add --no-cache curl # COPY package.json package-lock.json ./ COPY package.json ./ -RUN npm install --only=production && npm cache clean --force +RUN npm install --omit=dev && npm cache clean --force COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static diff --git a/LICENSE b/LICENSE index 8c5cfb8..0e38f56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,35 +1,5 @@ Copyright (c) 2025 Fossorial, LLC. -Portions of this software are licensed as follows: - -* All files that include a header specifying they are licensed under the - "Fossorial Commercial License" are governed by the Fossorial Commercial - License terms. The specific terms applicable to each customer depend on the - commercial license tier agreed upon in writing with Fossorial LLC. - Unauthorized use, copying, modification, or distribution is strictly - prohibited. - -* All files that include a header specifying they are licensed under the GNU - Affero General Public License, Version 3 ("AGPL-3"), are governed by the - AGPL-3 terms. A full copy of the AGPL-3 license is provided below. However, - these files are also available under the Fossorial Commercial License if a - separate commercial license agreement has been executed between the customer - and Fossorial LLC. - -* All files without a license header are, by default, licensed under the GNU - Affero General Public License, Version 3 (AGPL-3). These files may also be - made available under the Fossorial Commercial License upon agreement with - Fossorial LLC. - -* All third-party components included in this repository are licensed under - their respective original licenses, as provided by their authors. - -Please consult the header of each individual file to determine the applicable -license. For AGPL-3 licensed files, dual-licensing under the Fossorial -Commercial License is available subject to written agreement with Fossorial -LLC. - - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/README.md b/README.md index 15ca7ad..e513a13 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and access _Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._ +This is a fork of Pangolin with all proprietary code removed. Proprietary and paywalled features +will be reimplemented under the AGPL license. + ## Key Features ### Reverse Proxy Through WireGuard Tunnel diff --git a/package-lock.json b/package-lock.json index c6da917..20cb9e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -383,11 +383,30 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@emnapi/core": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", - "dev": true, + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", "license": "MIT", "optional": true, "dependencies": { @@ -1618,6 +1637,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", + "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@next/env": { "version": "15.2.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", @@ -1828,6 +1880,38 @@ "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", + "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-android-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", + "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/argon2-darwin-arm64": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", @@ -1844,6 +1928,387 @@ "node": ">= 10" } }, + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", + "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", + "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", + "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", + "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", + "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", + "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/bcrypt": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", @@ -3679,6 +4144,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", @@ -7522,6 +7997,13 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "license": "Unlicense", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -9183,6 +9665,29 @@ "node": ">= 0.6" } }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "license": "Unlicense", + "optional": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memfs-browser": { + "version": "3.5.10302", + "resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz", + "integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==", + "license": "Unlicense", + "optional": true, + "dependencies": { + "memfs": "3.5.3" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -12360,6 +12865,22 @@ "node": ">= 10" } }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", + "integrity": "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index e83031a..d974f03 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -1,7 +1,7 @@ import { Request } from "express"; import { db } from "@server/db"; import { userActions, roleActions, userOrgs } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -51,6 +51,7 @@ export enum ActionsEnum { listRoleResources = "listRoleResources", // listRoleActions = "listRoleActions", addUserRole = "addUserRole", + setUserRoles = "setUserRoles", // addUserSite = "addUserSite", // addUserAction = "addUserAction", // removeUserAction = "removeUserAction", @@ -106,29 +107,28 @@ export async function checkUserActionPermission( } try { - let userOrgRoleId = req.userOrgRoleId; + let userRoleIds = req.userRoleIds; // If userOrgRoleId is not available on the request, fetch it - if (userOrgRoleId === undefined) { - const userOrgRole = await db - .select() + if (userRoleIds === undefined) { + const userOrgRoles = await db + .select({ roleId: userOrgs.roleId }) .from(userOrgs) .where( and( eq(userOrgs.userId, userId), eq(userOrgs.orgId, req.userOrgId!) ) - ) - .limit(1); + ); - if (userOrgRole.length === 0) { + if (userOrgRoles.length === 0) { throw createHttpError( HttpCode.FORBIDDEN, "User does not have access to this organization" ); } - userOrgRoleId = userOrgRole[0].roleId; + userRoleIds = userOrgRoles.map((r) => r.roleId); } // Check if the user has direct permission for the action in the current org @@ -155,8 +155,8 @@ export async function checkUserActionPermission( .where( and( eq(roleActions.actionId, actionId), - eq(roleActions.roleId, userOrgRoleId!), - eq(roleActions.orgId, req.userOrgId!) + eq(roleActions.orgId, req.userOrgId!), + inArray(roleActions.roleId, userRoleIds!) ) ) .limit(1); diff --git a/server/auth/canUserAccessResource.ts b/server/auth/canUserAccessResource.ts index 0d61825..f322529 100644 --- a/server/auth/canUserAccessResource.ts +++ b/server/auth/canUserAccessResource.ts @@ -1,15 +1,15 @@ import db from "@server/db"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { roleResources, userResources } from "@server/db/schemas"; export async function canUserAccessResource({ userId, resourceId, - roleId + roleIds }: { userId: string; resourceId: number; - roleId: number; + roleIds: number[]; }): Promise { const roleResourceAccess = await db .select() @@ -17,7 +17,7 @@ export async function canUserAccessResource({ .where( and( eq(roleResources.resourceId, resourceId), - eq(roleResources.roleId, roleId) + inArray(roleResources.roleId, roleIds) ) ) .limit(1); diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index ebbc0ce..7c790eb 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -417,15 +417,6 @@ export const resourceRules = sqliteTable("resourceRules", { value: text("value").notNull() }); -export const supporterKey = sqliteTable("supporterKey", { - keyId: integer("keyId").primaryKey({ autoIncrement: true }), - key: text("key").notNull(), - githubUsername: text("githubUsername").notNull(), - phrase: text("phrase"), - tier: text("tier"), - valid: integer("valid", { mode: "boolean" }).notNull().default(false) -}); - // Identity Providers export const idp = sqliteTable("idp", { idpId: integer("idpId").primaryKey({ autoIncrement: true }), @@ -458,12 +449,6 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { scopes: text("scopes").notNull() }); -export const licenseKey = sqliteTable("licenseKey", { - licenseKeyId: text("licenseKeyId").primaryKey().notNull(), - instanceId: text("instanceId").notNull(), - token: text("token").notNull() -}); - export const hostMeta = sqliteTable("hostMeta", { hostMetaId: text("hostMetaId").primaryKey().notNull(), createdAt: integer("createdAt").notNull() @@ -543,8 +528,8 @@ export type ResourceWhitelist = InferSelectModel; export type VersionMigration = InferSelectModel; export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; -export type SupporterKey = InferSelectModel; export type Idp = InferSelectModel; +export type IdpOrg = InferSelectModel; export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; diff --git a/server/index.ts b/server/index.ts index 4c16caa..7dacae1 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,8 +5,7 @@ import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; -import { createIntegrationApiServer } from "./integrationApiServer"; -import license from "./license/license.js"; +// import { createIntegrationApiServer } from "./integrationApiServer"; async function startServers() { await runSetupFunctions(); @@ -17,9 +16,7 @@ async function startServers() { const nextServer = await createNextServer(); let integrationServer; - if (await license.isUnlocked()) { - integrationServer = createIntegrationApiServer(); - } + // integrationServer = createIntegrationApiServer(); return { apiServer, @@ -38,7 +35,7 @@ declare global { session?: Session; userOrg?: UserOrg; apiKeyOrg?: ApiKeyOrg; - userOrgRoleId?: number; + userRoleIds?: number[]; userOrgId?: string; userOrgIds?: string[]; } diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts deleted file mode 100644 index ff5dca5..0000000 --- a/server/integrationApiServer.ts +++ /dev/null @@ -1,110 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import express from "express"; -import cors from "cors"; -import cookieParser from "cookie-parser"; -import config from "@server/lib/config"; -import logger from "@server/logger"; -import { - errorHandlerMiddleware, - notFoundMiddleware, - verifyValidLicense -} from "@server/middlewares"; -import { authenticated, unauthenticated } from "@server/routers/integration"; -import { logIncomingMiddleware } from "./middlewares/logIncoming"; -import helmet from "helmet"; -import swaggerUi from "swagger-ui-express"; -import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; -import { registry } from "./openApi"; - -const dev = process.env.ENVIRONMENT !== "prod"; -const externalPort = config.getRawConfig().server.integration_port; - -export function createIntegrationApiServer() { - const apiServer = express(); - - apiServer.use(verifyValidLicense); - - if (config.getRawConfig().server.trust_proxy) { - apiServer.set("trust proxy", 1); - } - - apiServer.use(cors()); - - if (!dev) { - apiServer.use(helmet()); - } - - apiServer.use(cookieParser()); - apiServer.use(express.json()); - - apiServer.use( - "/v1/docs", - swaggerUi.serve, - swaggerUi.setup(getOpenApiDocumentation()) - ); - - // API routes - const prefix = `/v1`; - apiServer.use(logIncomingMiddleware); - apiServer.use(prefix, unauthenticated); - apiServer.use(prefix, authenticated); - - // Error handling - apiServer.use(notFoundMiddleware); - apiServer.use(errorHandlerMiddleware); - - // Create HTTP server - const httpServer = apiServer.listen(externalPort, (err?: any) => { - if (err) throw err; - logger.info( - `Integration API server is running on http://localhost:${externalPort}` - ); - }); - - return httpServer; -} - -function getOpenApiDocumentation() { - const bearerAuth = registry.registerComponent( - "securitySchemes", - "Bearer Auth", - { - type: "http", - scheme: "bearer" - } - ); - - for (const def of registry.definitions) { - if (def.type === "route") { - def.route.security = [ - { - [bearerAuth.name]: [] - } - ]; - } - } - - registry.registerPath({ - method: "get", - path: "/", - description: "Health check", - tags: [], - request: {}, - responses: {} - }); - - const generator = new OpenApiGeneratorV3(registry.definitions); - - return generator.generateDocument({ - openapi: "3.0.0", - info: { - version: "v1", - title: "Pangolin Integration API" - }, - servers: [{ url: "/v1" }] - }); -} diff --git a/server/lib/canUserAccessResource.ts b/server/lib/canUserAccessResource.ts index 0d61825..f322529 100644 --- a/server/lib/canUserAccessResource.ts +++ b/server/lib/canUserAccessResource.ts @@ -1,15 +1,15 @@ import db from "@server/db"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { roleResources, userResources } from "@server/db/schemas"; export async function canUserAccessResource({ userId, resourceId, - roleId + roleIds }: { userId: string; resourceId: number; - roleId: number; + roleIds: number[]; }): Promise { const roleResourceAccess = await db .select() @@ -17,7 +17,7 @@ export async function canUserAccessResource({ .where( and( eq(roleResources.resourceId, resourceId), - eq(roleResources.roleId, roleId) + inArray(roleResources.roleId, roleIds) ) ) .limit(1); diff --git a/server/lib/config.ts b/server/lib/config.ts index a19b4a2..1937d41 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -10,10 +10,6 @@ import { } from "@server/lib/consts"; import { passwordSchema } from "@server/auth/passwordSchema"; import stoi from "./stoi"; -import db from "@server/db"; -import { SupporterKey, supporterKey } from "@server/db/schemas"; -import { eq } from "drizzle-orm"; -import { license } from "@server/license/license"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -172,10 +168,6 @@ const configSchema = z.object({ export class Config { private rawConfig!: z.infer; - supporterData: SupporterKey | null = null; - - supporterHiddenUntil: number | null = null; - isDev: boolean = process.env.ENVIRONMENT !== "prod"; constructor() { @@ -264,20 +256,9 @@ export class Config { : "false"; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; - license.setServerSecret(parsedConfig.data.server.secret); - - this.checkKeyStatus(); - this.rawConfig = parsedConfig.data; } - private async checkKeyStatus() { - const licenseStatus = await license.check(); - if (!licenseStatus.isHostLicensed) { - this.checkSupporterKey(); - } - } - public getRawConfig() { return this.rawConfig; } @@ -291,90 +272,6 @@ export class Config { public getDomain(domainId: string) { return this.rawConfig.domains[domainId]; } - - public hideSupporterKey(days: number = 7) { - const now = new Date().getTime(); - - if (this.supporterHiddenUntil && now < this.supporterHiddenUntil) { - return; - } - - this.supporterHiddenUntil = now + 1000 * 60 * 60 * 24 * days; - } - - public isSupporterKeyHidden() { - const now = new Date().getTime(); - - if (this.supporterHiddenUntil && now < this.supporterHiddenUntil) { - return true; - } - - return false; - } - - public async checkSupporterKey() { - const [key] = await db.select().from(supporterKey).limit(1); - - if (!key) { - return; - } - - const { key: licenseKey, githubUsername } = key; - - try { - const response = await fetch( - "https://api.fossorial.io/api/v1/license/validate", - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - licenseKey, - githubUsername - }) - } - ); - - if (!response.ok) { - this.supporterData = key; - return; - } - - const data = await response.json(); - - if (!data.data.valid) { - this.supporterData = { - ...key, - valid: false - }; - return; - } - - this.supporterData = { - ...key, - tier: data.data.tier, - valid: true - }; - - // update the supporter key in the database - await db - .update(supporterKey) - .set({ - tier: data.data.tier || null, - phrase: data.data.cutePhrase || null, - valid: true - }) - .where(eq(supporterKey.keyId, key.keyId)); - } catch (e) { - this.supporterData = key; - console.error("Failed to validate supporter key", e); - } - } - - public getSupporterData() { - return this.supporterData; - } } export const config = new Config(); diff --git a/server/license/license.ts b/server/license/license.ts deleted file mode 100644 index e97b8f5..0000000 --- a/server/license/license.ts +++ /dev/null @@ -1,493 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import db from "@server/db"; -import { hostMeta, licenseKey, sites } from "@server/db/schemas"; -import logger from "@server/logger"; -import NodeCache from "node-cache"; -import { validateJWT } from "./licenseJwt"; -import { count, eq } from "drizzle-orm"; -import moment from "moment"; -import { setHostMeta } from "@server/setup/setHostMeta"; -import { encrypt, decrypt } from "@server/lib/crypto"; - -const keyTypes = ["HOST", "SITES"] as const; -type KeyType = (typeof keyTypes)[number]; - -const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const; -type KeyTier = (typeof keyTiers)[number]; - -export type LicenseStatus = { - isHostLicensed: boolean; // Are there any license keys? - isLicenseValid: boolean; // Is the license key valid? - hostId: string; // Host ID - maxSites?: number; - usedSites?: number; - tier?: KeyTier; -}; - -export type LicenseKeyCache = { - licenseKey: string; - licenseKeyEncrypted: string; - valid: boolean; - iat?: Date; - type?: KeyType; - tier?: KeyTier; - numSites?: number; -}; - -type ActivateLicenseKeyAPIResponse = { - data: { - instanceId: string; - }; - success: boolean; - error: string; - message: string; - status: number; -}; - -type ValidateLicenseAPIResponse = { - data: { - licenseKeys: { - [key: string]: string; - }; - }; - success: boolean; - error: string; - message: string; - status: number; -}; - -type TokenPayload = { - valid: boolean; - type: KeyType; - tier: KeyTier; - quantity: number; - terminateAt: string; // ISO - iat: number; // Issued at -}; - -export class License { - private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds - private validationServerUrl = - "https://api.fossorial.io/api/v1/license/professional/validate"; - private activationServerUrl = - "https://api.fossorial.io/api/v1/license/professional/activate"; - - private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); - private licenseKeyCache = new NodeCache(); - - private ephemeralKey!: string; - private statusKey = "status"; - private serverSecret!: string; - - private publicKey = `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF -FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf -CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl -apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt -h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y -zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y -LQIDAQAB ------END PUBLIC KEY-----`; - - constructor(private hostId: string) { - this.ephemeralKey = Buffer.from( - JSON.stringify({ ts: new Date().toISOString() }) - ).toString("base64"); - - setInterval( - async () => { - await this.check(); - }, - 1000 * 60 * 60 - ); // 1 hour = 60 * 60 = 3600 seconds - } - - public listKeys(): LicenseKeyCache[] { - const keys = this.licenseKeyCache.keys(); - return keys.map((key) => { - return this.licenseKeyCache.get(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 { - const status = await this.check(); - if (status.isHostLicensed) { - if (status.isLicenseValid) { - return true; - } - } - return false; - } - - public async check(): Promise { - // 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( - decryptedToken, - this.publicKey - ); - - this.licenseKeyCache.set(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( - 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( - 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( - key.licenseKey, - cached - ); - continue; - } - - const payload = validateJWT( - 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( - 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( - 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( - 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 { - // 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; diff --git a/server/license/licenseJwt.ts b/server/license/licenseJwt.ts deleted file mode 100644 index ed7f4a0..0000000 --- a/server/license/licenseJwt.ts +++ /dev/null @@ -1,114 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import * as crypto from "crypto"; - -/** - * Validates a JWT using a public key - * @param token - The JWT to validate - * @param publicKey - The public key used for verification (PEM format) - * @returns The decoded payload if validation succeeds, throws an error otherwise - */ -function validateJWT( - 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 }; diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 03d6f3b..6dbdcd6 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -16,7 +16,6 @@ export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; export * from "./verifyUserIsServerAdmin"; export * from "./verifyIsLoggedInUser"; -export * from "./integration"; -export * from "./verifyValidLicense"; +// export * from "./integration"; export * from "./verifyUserHasAction"; -export * from "./verifyApiKeyAccess"; +// export * from "./verifyApiKeyAccess"; diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts deleted file mode 100644 index c16e129..0000000 --- a/server/middlewares/integration/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -export * from "./verifyApiKey"; -export * from "./verifyApiKeyOrgAccess"; -export * from "./verifyApiKeyHasAction"; -export * from "./verifyApiKeySiteAccess"; -export * from "./verifyApiKeyResourceAccess"; -export * from "./verifyApiKeyTargetAccess"; -export * from "./verifyApiKeyRoleAccess"; -export * from "./verifyApiKeyUserAccess"; -export * from "./verifyApiKeySetResourceUsers"; -export * from "./verifyAccessTokenAccess"; -export * from "./verifyApiKeyIsRoot"; -export * from "./verifyApiKeyApiKeyAccess"; diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts deleted file mode 100644 index 82badcd..0000000 --- a/server/middlewares/integration/verifyAccessTokenAccess.ts +++ /dev/null @@ -1,115 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyAccessTokenAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const accessTokenId = req.params.accessTokenId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - const [accessToken] = await db - .select() - .from(resourceAccessToken) - .where(eq(resourceAccessToken.accessTokenId, accessTokenId)) - .limit(1); - - if (!accessToken) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Access token with ID ${accessTokenId} not found` - ) - ); - } - - const resourceId = accessToken.resourceId; - - if (!resourceId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Access token with ID ${accessTokenId} does not have a resource ID` - ) - ); - } - - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (!resource.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` - ) - ); - } - - // Verify that the API key is linked to the resource's organization - if (!req.apiKeyOrg) { - const apiKeyOrgResult = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, resource.orgId) - ) - ) - .limit(1); - - if (apiKeyOrgResult.length > 0) { - req.apiKeyOrg = apiKeyOrgResult[0]; - } - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - - return next(); - } catch (e) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying access token access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKey.ts b/server/middlewares/integration/verifyApiKey.ts deleted file mode 100644 index 39fc3de..0000000 --- a/server/middlewares/integration/verifyApiKey.ts +++ /dev/null @@ -1,65 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { verifyPassword } from "@server/auth/password"; -import db from "@server/db"; -import { apiKeys } from "@server/db/schemas"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import { eq } from "drizzle-orm"; -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; - -export async function verifyApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts deleted file mode 100644 index aedc60c..0000000 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ /dev/null @@ -1,86 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { apiKeys, apiKeyOrg } from "@server/db/schemas"; -import { and, eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyApiKeyAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const {apiKey: callerApiKey } = req; - - const apiKeyId = - req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; - - if (!callerApiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (!apiKeyId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") - ); - } - - const [callerApiKeyOrg] = await db - .select() - .from(apiKeyOrg) - .where( - and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId)) - ) - .limit(1); - - if (!callerApiKeyOrg) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `API key with ID ${apiKeyId} does not have an organization ID` - ) - ); - } - - const [otherApiKeyOrg] = await db - .select() - .from(apiKeyOrg) - .where( - and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId)) - ) - .limit(1); - - if (!otherApiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - `API key with ID ${apiKeyId} does not have access to organization with ID ${orgId}` - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying key access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyHasAction.ts b/server/middlewares/integration/verifyApiKeyHasAction.ts deleted file mode 100644 index 0326c46..0000000 --- a/server/middlewares/integration/verifyApiKeyHasAction.ts +++ /dev/null @@ -1,61 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; -import { ActionsEnum } from "@server/auth/actions"; -import db from "@server/db"; -import { apiKeyActions } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; - -export function verifyApiKeyHasAction(action: ActionsEnum) { - return async function ( - req: Request, - res: Response, - next: NextFunction - ): Promise { - 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" - ) - ); - } - }; -} diff --git a/server/middlewares/integration/verifyApiKeyIsRoot.ts b/server/middlewares/integration/verifyApiKeyIsRoot.ts deleted file mode 100644 index 35cd0fa..0000000 --- a/server/middlewares/integration/verifyApiKeyIsRoot.ts +++ /dev/null @@ -1,44 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; - -export async function verifyApiKeyIsRoot( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts deleted file mode 100644 index e1e1e0d..0000000 --- a/server/middlewares/integration/verifyApiKeyOrgAccess.ts +++ /dev/null @@ -1,66 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { apiKeyOrg } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; - -export async function verifyApiKeyOrgAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKeyId = req.apiKey?.apiKeyId; - const orgId = req.params.orgId; - - if (!apiKeyId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ); - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - if (!req.apiKeyOrg) { - next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (e) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying organization access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyResourceAccess.ts b/server/middlewares/integration/verifyApiKeyResourceAccess.ts deleted file mode 100644 index 49180b5..0000000 --- a/server/middlewares/integration/verifyApiKeyResourceAccess.ts +++ /dev/null @@ -1,90 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { resources, apiKeyOrg } from "@server/db/schemas"; -import { eq, and } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyResourceAccess( - req: Request, - res: Response, - next: NextFunction -) { - const apiKey = req.apiKey; - const resourceId = - req.params.resourceId || req.body.resourceId || req.query.resourceId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - try { - // Retrieve the resource - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (!resource.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` - ) - ); - } - - // Verify that the API key is linked to the resource's organization - if (!req.apiKeyOrg) { - const apiKeyOrgResult = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, resource.orgId) - ) - ) - .limit(1); - - if (apiKeyOrgResult.length > 0) { - req.apiKeyOrg = apiKeyOrgResult[0]; - } - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying resource access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts deleted file mode 100644 index a7abf9a..0000000 --- a/server/middlewares/integration/verifyApiKeyRoleAccess.ts +++ /dev/null @@ -1,132 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { roles, apiKeyOrg } from "@server/db/schemas"; -import { and, eq, inArray } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; - -export async function verifyApiKeyRoleAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const singleRoleId = parseInt( - req.params.roleId || req.body.roleId || req.query.roleId - ); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - const { roleIds } = req.body; - const allRoleIds = - roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]); - - if (allRoleIds.length === 0) { - return next(); - } - - const rolesData = await db - .select() - .from(roles) - .where(inArray(roles.roleId, allRoleIds)); - - if (rolesData.length !== allRoleIds.length) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "One or more roles not found" - ) - ); - } - - const orgIds = new Set(rolesData.map((role) => role.orgId)); - - for (const role of rolesData) { - const apiKeyOrgAccess = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, role.orgId!) - ) - ) - .limit(1); - - if (apiKeyOrgAccess.length === 0) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - `Key does not have access to organization for role ID ${role.roleId}` - ) - ); - } - } - - if (orgIds.size > 1) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Roles must belong to the same organization" - ) - ); - } - - const orgId = orgIds.values().next().value; - - if (!orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Roles do not have an organization ID" - ) - ); - } - - if (!req.apiKeyOrg) { - // Retrieve the API key's organization link if not already set - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ) - .limit(1); - - if (apiKeyOrgRes.length === 0) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - return next(); - } catch (error) { - logger.error("Error verifying role access:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying role access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts deleted file mode 100644 index d43021b..0000000 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ /dev/null @@ -1,74 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs } from "@server/db/schemas"; -import { and, eq, inArray } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeySetResourceUsers( - req: Request, - res: Response, - next: NextFunction -) { - const apiKey = req.apiKey; - const userIds = req.body.userIds; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - if (!userIds) { - return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs")); - } - - if (userIds.length === 0) { - return next(); - } - - try { - const orgId = req.apiKeyOrg.orgId; - const userOrgsData = await db - .select() - .from(userOrgs) - .where( - and( - inArray(userOrgs.userId, userIds), - eq(userOrgs.orgId, orgId) - ) - ); - - if (userOrgsData.length !== userIds.length) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to one or more specified users" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error checking if key has access to the specified users" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySiteAccess.ts b/server/middlewares/integration/verifyApiKeySiteAccess.ts deleted file mode 100644 index 7d10dde..0000000 --- a/server/middlewares/integration/verifyApiKeySiteAccess.ts +++ /dev/null @@ -1,94 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { - sites, - apiKeyOrg -} from "@server/db/schemas"; -import { and, eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeySiteAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const siteId = parseInt( - req.params.siteId || req.body.siteId || req.query.siteId - ); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (isNaN(siteId)) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID") - ); - } - - const site = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (site.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` - ) - ); - } - - if (!site[0].orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Site with ID ${siteId} does not have an organization ID` - ) - ); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, site[0].orgId) - ) - ); - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying site access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts deleted file mode 100644 index bd6e5bc..0000000 --- a/server/middlewares/integration/verifyApiKeyTargetAccess.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { resources, targets, apiKeyOrg } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyTargetAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const targetId = parseInt(req.params.targetId); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (isNaN(targetId)) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID") - ); - } - - const [target] = await db - .select() - .from(targets) - .where(eq(targets.targetId, targetId)) - .limit(1); - - if (!target) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Target with ID ${targetId} not found` - ) - ); - } - - const resourceId = target.resourceId; - if (!resourceId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Target with ID ${targetId} does not have a resource ID` - ) - ); - } - - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (!resource.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` - ) - ); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgResult = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, resource.orgId) - ) - ) - .limit(1); - if (apiKeyOrgResult.length > 0) { - req.apiKeyOrg = apiKeyOrgResult[0]; - } - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying target access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyUserAccess.ts b/server/middlewares/integration/verifyApiKeyUserAccess.ts deleted file mode 100644 index e1b5d3d..0000000 --- a/server/middlewares/integration/verifyApiKeyUserAccess.ts +++ /dev/null @@ -1,72 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyUserAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const reqUserId = - req.params.userId || req.body.userId || req.query.userId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!reqUserId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID") - ); - } - - if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have organization access" - ) - ); - } - - const orgId = req.apiKeyOrg.orgId; - - const [userOrgRecord] = await db - .select() - .from(userOrgs) - .where( - and(eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, orgId)) - ) - .limit(1); - - if (!userOrgRecord) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this user" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error checking if key has access to this user" - ) - ); - } -} diff --git a/server/middlewares/verifyAccessTokenAccess.ts b/server/middlewares/verifyAccessTokenAccess.ts index a437a8a..66c8439 100644 --- a/server/middlewares/verifyAccessTokenAccess.ts +++ b/server/middlewares/verifyAccessTokenAccess.ts @@ -82,24 +82,24 @@ export async function verifyAccessTokenAccess( ) ); req.userOrg = res[0]; + req.userRoleIds = res.map((r) => r.roleId); } if (!req.userOrg) { - next( + return next( createHttpError( HttpCode.FORBIDDEN, "User does not have access to this organization" ) ); - } else { - req.userOrgRoleId = req.userOrg.roleId; - req.userOrgId = resource[0].orgId!; } + req.userOrgId = resource[0].orgId!; + const resourceAllowed = await canUserAccessResource({ userId, resourceId, - roleId: req.userOrgRoleId! + roleIds: req.userRoleIds! }); if (!resourceAllowed) { diff --git a/server/middlewares/verifyAdmin.ts b/server/middlewares/verifyAdmin.ts index b53f238..240888e 100644 --- a/server/middlewares/verifyAdmin.ts +++ b/server/middlewares/verifyAdmin.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; import { roles, userOrgs } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -29,9 +29,11 @@ export async function verifyAdmin( const userOrgRes = await db .select() .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!))) - .limit(1); + .where( + and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)) + ); req.userOrg = userOrgRes[0]; + req.userRoleIds = userOrgRes.map((r) => r.roleId); } if (!req.userOrg) { @@ -43,13 +45,13 @@ export async function verifyAdmin( ); } - const userRole = await db + const userAdminRole = await db .select() .from(roles) - .where(eq(roles.roleId, req.userOrg.roleId)) + .where(and(inArray(roles.roleId, req.userRoleIds!), roles.isAdmin)) .limit(1); - if (userRole.length === 0 || !userRole[0].isAdmin) { + if (userAdminRole.length === 0) { return next( createHttpError( HttpCode.FORBIDDEN, diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts deleted file mode 100644 index 0bba8f4..0000000 --- a/server/middlewares/verifyApiKeyAccess.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas"; -import { and, eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const userId = req.user!.userId; - const apiKeyId = - req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (!apiKeyId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") - ); - } - - const [apiKey] = await db - .select() - .from(apiKeys) - .innerJoin(apiKeyOrg, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId)) - .where( - and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId)) - ) - .limit(1); - - if (!apiKey.apiKeys) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `API key with ID ${apiKeyId} not found` - ) - ); - } - - if (!apiKeyOrg.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `API key with ID ${apiKeyId} does not have an organization ID` - ) - ); - } - - if (!req.userOrg) { - const userOrgRole = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, apiKeyOrg.orgId) - ) - ) - .limit(1); - req.userOrg = userOrgRole[0]; - } - - if (!req.userOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) - ); - } - - const userOrgRoleId = req.userOrg.roleId; - req.userOrgRoleId = userOrgRoleId; - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying key access" - ) - ); - } -} diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts index 20018e0..9af4fe5 100644 --- a/server/middlewares/verifyOrgAccess.ts +++ b/server/middlewares/verifyOrgAccess.ts @@ -34,21 +34,20 @@ export async function verifyOrgAccess( and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) ); req.userOrg = userOrgRes[0]; + req.userRoleIds = userOrgRes.map((r) => r.roleId); } if (!req.userOrg) { - next( + return next( createHttpError( HttpCode.FORBIDDEN, "User does not have access to this organization" ) ); - } else { - // User has access, attach the user's role to the request for potential future use - req.userOrgRoleId = req.userOrg.roleId; - req.userOrgId = orgId; - return next(); } + + req.userOrgId = orgId; + return next(); } catch (e) { return next( createHttpError( diff --git a/server/middlewares/verifyResourceAccess.ts b/server/middlewares/verifyResourceAccess.ts index dc5fcc2..43ab908 100644 --- a/server/middlewares/verifyResourceAccess.ts +++ b/server/middlewares/verifyResourceAccess.ts @@ -4,9 +4,9 @@ import { resources, userOrgs, userResources, - roleResources, + roleResources } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -59,9 +59,9 @@ export async function verifyResourceAccess( eq(userOrgs.userId, userId), eq(userOrgs.orgId, resource[0].orgId) ) - ) - .limit(1); + ); req.userOrg = userOrgRole[0]; + req.userRoleIds = userOrgRole.map((r) => r.roleId); } if (!req.userOrg) { @@ -73,8 +73,6 @@ export async function verifyResourceAccess( ); } - const userOrgRoleId = req.userOrg.roleId; - req.userOrgRoleId = userOrgRoleId; req.userOrgId = resource[0].orgId; const roleResourceAccess = await db @@ -83,7 +81,7 @@ export async function verifyResourceAccess( .where( and( eq(roleResources.resourceId, resourceId), - eq(roleResources.roleId, userOrgRoleId) + inArray(roleResources.roleId, req.userRoleIds!) ) ) .limit(1); diff --git a/server/middlewares/verifyRoleAccess.ts b/server/middlewares/verifyRoleAccess.ts index 5491704..fac348d 100644 --- a/server/middlewares/verifyRoleAccess.ts +++ b/server/middlewares/verifyRoleAccess.ts @@ -98,11 +98,10 @@ export async function verifyRoleAccess( .from(userOrgs) .where( and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)) - ) - .limit(1); + ); req.userOrg = userOrg[0]; - req.userOrgRoleId = userOrg[0].roleId; + req.userRoleIds = userOrg.map((r) => r.roleId); } return next(); diff --git a/server/middlewares/verifySiteAccess.ts b/server/middlewares/verifySiteAccess.ts index b741e3a..640985d 100644 --- a/server/middlewares/verifySiteAccess.ts +++ b/server/middlewares/verifySiteAccess.ts @@ -5,9 +5,9 @@ import { userOrgs, userSites, roleSites, - roles, + roles } from "@server/db/schemas"; -import { and, eq, or } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import logger from "@server/logger"; @@ -71,6 +71,7 @@ export async function verifySiteAccess( ) .limit(1); req.userOrg = userOrgRole[0]; + req.userRoleIds = userOrgRole.map((r) => r.roleId); } if (!req.userOrg) { @@ -82,8 +83,6 @@ export async function verifySiteAccess( ); } - const userOrgRoleId = req.userOrg.roleId; - req.userOrgRoleId = userOrgRoleId; req.userOrgId = site[0].orgId; // Check role-based site access first @@ -93,7 +92,7 @@ export async function verifySiteAccess( .where( and( eq(roleSites.siteId, siteId), - eq(roleSites.roleId, userOrgRoleId) + inArray(roleSites.roleId, req.userRoleIds!) ) ) .limit(1); diff --git a/server/middlewares/verifyTargetAccess.ts b/server/middlewares/verifyTargetAccess.ts index f57ba47..4065ce5 100644 --- a/server/middlewares/verifyTargetAccess.ts +++ b/server/middlewares/verifyTargetAccess.ts @@ -88,24 +88,23 @@ export async function verifyTargetAccess( ) ); req.userOrg = res[0]; + req.userRoleIds = res.map((r) => r.roleId); } if (!req.userOrg) { - next( + return next( createHttpError( HttpCode.FORBIDDEN, "User does not have access to this organization" ) ); - } else { - req.userOrgRoleId = req.userOrg.roleId; - req.userOrgId = resource[0].orgId!; } + req.userOrgId = resource[0].orgId!; const resourceAllowed = await canUserAccessResource({ userId, resourceId, - roleId: req.userOrgRoleId! + roleIds: req.userRoleIds! }); if (!resourceAllowed) { diff --git a/server/middlewares/verifyUserAccess.ts b/server/middlewares/verifyUserAccess.ts index 43ec9cf..9cc30cf 100644 --- a/server/middlewares/verifyUserAccess.ts +++ b/server/middlewares/verifyUserAccess.ts @@ -33,9 +33,9 @@ export async function verifyUserAccess( eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, req.userOrgId!) ) - ) - .limit(1); + ); req.userOrg = res[0]; + req.userRoleIds = res.map((r) => r.roleId); } if (!req.userOrg) { diff --git a/server/middlewares/verifyUserInRole.ts b/server/middlewares/verifyUserInRole.ts index 2a15311..d833d0a 100644 --- a/server/middlewares/verifyUserInRole.ts +++ b/server/middlewares/verifyUserInRole.ts @@ -12,7 +12,7 @@ export async function verifyUserInRole( const roleId = parseInt( req.params.roleId || req.body.roleId || req.query.roleId ); - const userRoleId = req.userOrgRoleId; + const userRoleIds = req.userRoleIds; if (isNaN(roleId)) { return next( @@ -20,7 +20,7 @@ export async function verifyUserInRole( ); } - if (!userRoleId) { + if (!userRoleIds) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -29,7 +29,7 @@ export async function verifyUserInRole( ); } - if (userRoleId !== roleId) { + if (userRoleIds.indexOf(roleId) === -1) { return next( createHttpError( HttpCode.FORBIDDEN, diff --git a/server/middlewares/verifyUserIsOrgOwner.ts b/server/middlewares/verifyUserIsOrgOwner.ts index ac96f37..c1d766e 100644 --- a/server/middlewares/verifyUserIsOrgOwner.ts +++ b/server/middlewares/verifyUserIsOrgOwner.ts @@ -36,6 +36,7 @@ export async function verifyUserIsOrgOwner( and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) ); req.userOrg = res[0]; + req.userRoleIds = res.map((r) => r.roleId); } if (!req.userOrg) { diff --git a/server/middlewares/verifyValidLicense.ts b/server/middlewares/verifyValidLicense.ts deleted file mode 100644 index 7f4de34..0000000 --- a/server/middlewares/verifyValidLicense.ts +++ /dev/null @@ -1,33 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import license from "@server/license/license"; - -export async function verifyValidLicense( - req: Request, - res: Response, - next: NextFunction -) { - try { - const unlocked = await license.isUnlocked(); - if (!unlocked) { - return next( - createHttpError(HttpCode.FORBIDDEN, "License is not valid") - ); - } - - return next(); - } catch (e) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying license" - ) - ); - } -} diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index 07ef9aa..daa09a4 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -208,7 +208,7 @@ export async function listAccessTokens( .where( or( eq(userResources.userId, req.user!.userId), - eq(roleResources.roleId, req.userOrgRoleId!) + inArray(roleResources.roleId, req.userRoleIds!) ) ); } else { diff --git a/server/routers/apiKeys/createOrgApiKey.ts b/server/routers/apiKeys/createOrgApiKey.ts deleted file mode 100644 index 2fb9fd2..0000000 --- a/server/routers/apiKeys/createOrgApiKey.ts +++ /dev/null @@ -1,133 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { NextFunction, Request, Response } from "express"; -import db from "@server/db"; -import HttpCode from "@server/types/HttpCode"; -import { z } from "zod"; -import { apiKeyOrg, apiKeys } from "@server/db/schemas"; -import { fromError } from "zod-validation-error"; -import createHttpError from "http-errors"; -import response from "@server/lib/response"; -import moment from "moment"; -import { - generateId, - generateIdFromEntropySize -} from "@server/auth/sessions/app"; -import logger from "@server/logger"; -import { hashPassword } from "@server/auth/password"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z.object({ - orgId: z.string().nonempty() -}); - -const bodySchema = z.object({ - name: z.string().min(1).max(255) -}); - -export type CreateOrgApiKeyBody = z.infer; - -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 { - 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(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" - ) - ); - } -} diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts deleted file mode 100644 index 775ae57..0000000 --- a/server/routers/apiKeys/createRootApiKey.ts +++ /dev/null @@ -1,105 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { NextFunction, Request, Response } from "express"; -import db from "@server/db"; -import HttpCode from "@server/types/HttpCode"; -import { z } from "zod"; -import { apiKeyOrg, apiKeys, orgs } from "@server/db/schemas"; -import { fromError } from "zod-validation-error"; -import createHttpError from "http-errors"; -import response from "@server/lib/response"; -import moment from "moment"; -import { - generateId, - generateIdFromEntropySize -} from "@server/auth/sessions/app"; -import logger from "@server/logger"; -import { hashPassword } from "@server/auth/password"; - -const bodySchema = z - .object({ - name: z.string().min(1).max(255) - }) - .strict(); - -export type CreateRootApiKeyBody = z.infer; - -export type CreateRootApiKeyResponse = { - apiKeyId: string; - name: string; - apiKey: string; - lastChars: string; - createdAt: string; -}; - -export async function createRootApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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" - ) - ); - } -} diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts deleted file mode 100644 index 2af4ae2..0000000 --- a/server/routers/apiKeys/deleteApiKey.ts +++ /dev/null @@ -1,81 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeys } from "@server/db/schemas"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -registry.registerPath({ - method: "delete", - path: "/org/{orgId}/api-key/{apiKeyId}", - description: "Delete an API key.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema - }, - responses: {} -}); - -export async function deleteApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/apiKeys/deleteOrgApiKey.ts b/server/routers/apiKeys/deleteOrgApiKey.ts deleted file mode 100644 index 1834c82..0000000 --- a/server/routers/apiKeys/deleteOrgApiKey.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeyOrg, apiKeys } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty(), - orgId: z.string().nonempty() -}); - -export async function deleteOrgApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/apiKeys/getApiKey.ts b/server/routers/apiKeys/getApiKey.ts deleted file mode 100644 index bd495bd..0000000 --- a/server/routers/apiKeys/getApiKey.ts +++ /dev/null @@ -1,81 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeys } from "@server/db/schemas"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -async function query(apiKeyId: string) { - return await db - .select({ - apiKeyId: apiKeys.apiKeyId, - lastChars: apiKeys.lastChars, - createdAt: apiKeys.createdAt, - isRoot: apiKeys.isRoot, - name: apiKeys.name - }) - .from(apiKeys) - .where(eq(apiKeys.apiKeyId, apiKeyId)) - .limit(1); -} - -export type GetApiKeyResponse = NonNullable< - Awaited>[0] ->; - -export async function getApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/index.ts b/server/routers/apiKeys/index.ts deleted file mode 100644 index 84d4ee6..0000000 --- a/server/routers/apiKeys/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -export * from "./createRootApiKey"; -export * from "./deleteApiKey"; -export * from "./getApiKey"; -export * from "./listApiKeyActions"; -export * from "./listOrgApiKeys"; -export * from "./listApiKeyActions"; -export * from "./listRootApiKeys"; -export * from "./setApiKeyActions"; -export * from "./setApiKeyOrgs"; -export * from "./createOrgApiKey"; -export * from "./deleteOrgApiKey"; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts deleted file mode 100644 index 0cf694a..0000000 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ /dev/null @@ -1,118 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { db } from "@server/db"; -import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { eq } from "drizzle-orm"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -const querySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -function queryActions(apiKeyId: string) { - return db - .select({ - actionId: actions.actionId - }) - .from(apiKeyActions) - .where(eq(apiKeyActions.apiKeyId, apiKeyId)) - .innerJoin(actions, eq(actions.actionId, apiKeyActions.actionId)); -} - -export type ListApiKeyActionsResponse = { - actions: Awaited>; - 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 { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts deleted file mode 100644 index a016907..0000000 --- a/server/routers/apiKeys/listOrgApiKeys.ts +++ /dev/null @@ -1,121 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { db } from "@server/db"; -import { apiKeyOrg, apiKeys } from "@server/db/schemas"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { eq, and } from "drizzle-orm"; -import { OpenAPITags, registry } from "@server/openApi"; - -const querySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -const paramsSchema = z.object({ - orgId: z.string() -}); - -function queryApiKeys(orgId: string) { - return db - .select({ - apiKeyId: apiKeys.apiKeyId, - orgId: apiKeyOrg.orgId, - lastChars: apiKeys.lastChars, - createdAt: apiKeys.createdAt, - name: apiKeys.name - }) - .from(apiKeyOrg) - .where(and(eq(apiKeyOrg.orgId, orgId), eq(apiKeys.isRoot, false))) - .innerJoin(apiKeys, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId)); -} - -export type ListOrgApiKeysResponse = { - apiKeys: Awaited>; - 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 { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts deleted file mode 100644 index 7feca73..0000000 --- a/server/routers/apiKeys/listRootApiKeys.ts +++ /dev/null @@ -1,90 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { db } from "@server/db"; -import { apiKeys } from "@server/db/schemas"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { eq } from "drizzle-orm"; - -const querySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -function queryApiKeys() { - return db - .select({ - apiKeyId: apiKeys.apiKeyId, - lastChars: apiKeys.lastChars, - createdAt: apiKeys.createdAt, - name: apiKeys.name - }) - .from(apiKeys) - .where(eq(apiKeys.isRoot, true)); -} - -export type ListRootApiKeysResponse = { - apiKeys: Awaited>; - pagination: { total: number; limit: number; offset: number }; -}; - -export async function listRootApiKeys( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts deleted file mode 100644 index 187dd11..0000000 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ /dev/null @@ -1,141 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { actions, apiKeyActions } from "@server/db/schemas"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { eq, and, inArray } from "drizzle-orm"; -import { OpenAPITags, registry } from "@server/openApi"; - -const bodySchema = z - .object({ - actionIds: z - .array(z.string().nonempty()) - .transform((v) => Array.from(new Set(v))) - }) - .strict(); - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -registry.registerPath({ - method: "post", - path: "/org/{orgId}/api-key/{apiKeyId}/actions", - description: - "Set actions for an API key. This will replace any existing actions.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} -}); - -export async function setApiKeyActions( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts deleted file mode 100644 index ee0611d..0000000 --- a/server/routers/apiKeys/setApiKeyOrgs.ts +++ /dev/null @@ -1,122 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeyOrg, orgs } from "@server/db/schemas"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { eq, and, inArray } from "drizzle-orm"; - -const bodySchema = z - .object({ - orgIds: z - .array(z.string().nonempty()) - .transform((v) => Array.from(new Set(v))) - }) - .strict(); - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -export async function setApiKeyOrgs( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/external.ts b/server/routers/external.ts index d631c37..96f569b 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -8,11 +8,9 @@ import * as target from "./target"; import * as user from "./user"; import * as auth from "./auth"; import * as role from "./role"; -import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; -import * as license from "./license"; -import * as apiKeys from "./apiKeys"; +// import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -28,9 +26,8 @@ import { verifyUserAccess, getUserOrgs, verifyUserIsServerAdmin, - verifyIsLoggedInUser, - verifyApiKeyAccess, - verifyValidLicense + verifyIsLoggedInUser + // verifyApiKeyAccess } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -276,6 +273,14 @@ authenticated.get( verifyUserHasAction(ActionsEnum.listRoles), role.listRoles ); +authenticated.post( + "/org/:orgId/user/:userId/roles", + verifyOrgAccess, + verifyUserAccess, + verifyUserHasAction(ActionsEnum.setUserRoles), + user.setUserRoles +); + // authenticated.get( // "/role/:roleId", // verifyRoleAccess, @@ -406,12 +411,6 @@ authenticated.get( authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview); -authenticated.post( - `/supporter-key/validate`, - supporterKey.validateSupporterKey -); -authenticated.post(`/supporter-key/hide`, supporterKey.hideSupporterKey); - unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); // authenticated.get( @@ -531,28 +530,24 @@ authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); authenticated.put( "/idp/:idpId/org/:orgId", - verifyValidLicense, verifyUserIsServerAdmin, idp.createIdpOrgPolicy ); authenticated.post( "/idp/:idpId/org/:orgId", - verifyValidLicense, verifyUserIsServerAdmin, idp.updateIdpOrgPolicy ); authenticated.delete( "/idp/:idpId/org/:orgId", - verifyValidLicense, verifyUserIsServerAdmin, idp.deleteIdpOrgPolicy ); authenticated.get( "/idp/:idpId/org", - verifyValidLicense, verifyUserIsServerAdmin, idp.listIdpOrgPolicies ); @@ -560,75 +555,45 @@ authenticated.get( authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); -authenticated.post( - "/license/activate", - verifyUserIsServerAdmin, - license.activateLicense -); - -authenticated.get( - "/license/keys", - verifyUserIsServerAdmin, - license.listLicenseKeys -); - -authenticated.delete( - "/license/:licenseKey", - verifyUserIsServerAdmin, - license.deleteLicenseKey -); - -authenticated.post( - "/license/recheck", - verifyUserIsServerAdmin, - license.recheckStatus -); - +/* authenticated.get( `/api-key/:apiKeyId`, - verifyValidLicense, verifyUserIsServerAdmin, apiKeys.getApiKey ); authenticated.put( `/api-key`, - verifyValidLicense, verifyUserIsServerAdmin, apiKeys.createRootApiKey ); authenticated.delete( `/api-key/:apiKeyId`, - verifyValidLicense, verifyUserIsServerAdmin, apiKeys.deleteApiKey ); authenticated.get( `/api-keys`, - verifyValidLicense, verifyUserIsServerAdmin, apiKeys.listRootApiKeys ); authenticated.get( `/api-key/:apiKeyId/actions`, - verifyValidLicense, verifyUserIsServerAdmin, apiKeys.listApiKeyActions ); authenticated.post( `/api-key/:apiKeyId/actions`, - verifyValidLicense, verifyUserIsServerAdmin, apiKeys.setApiKeyActions ); authenticated.get( `/org/:orgId/api-keys`, - verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.listApiKeys), apiKeys.listOrgApiKeys @@ -636,7 +601,6 @@ authenticated.get( authenticated.post( `/org/:orgId/api-key/:apiKeyId/actions`, - verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.setApiKeyActions), @@ -645,7 +609,6 @@ authenticated.post( authenticated.get( `/org/:orgId/api-key/:apiKeyId/actions`, - verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.listApiKeyActions), @@ -654,7 +617,6 @@ authenticated.get( authenticated.put( `/org/:orgId/api-key`, - verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.createApiKey), apiKeys.createOrgApiKey @@ -662,7 +624,6 @@ authenticated.put( authenticated.delete( `/org/:orgId/api-key/:apiKeyId`, - verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.deleteApiKey), @@ -671,12 +632,12 @@ authenticated.delete( authenticated.get( `/org/:orgId/api-key/:apiKeyId`, - verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.getApiKey), apiKeys.getApiKey ); +*/ // Auth routes export const authRouter = Router(); diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index ae5acce..4f976b4 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -1,8 +1,3 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; @@ -12,7 +7,6 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import config from "@server/lib/config"; import { eq, and } from "drizzle-orm"; import { idp, idpOrg } from "@server/db/schemas"; @@ -56,16 +50,6 @@ export async function createIdpOrgPolicy( next: NextFunction ): Promise { try { - const parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - const parsedParams = paramsSchema.safeParse(req.params); if (!parsedParams.success) { return next( @@ -75,10 +59,20 @@ export async function createIdpOrgPolicy( ) ); } - const { idpId, orgId } = parsedParams.data; - const { roleMapping, orgMapping } = parsedBody.data; + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + let { orgMapping, roleMapping } = parsedBody.data; + + // Given identity provider must exist and not have a policy already const [existing] = await db .select() .from(idp) @@ -90,18 +84,15 @@ export async function createIdpOrgPolicy( if (!existing?.idp) { return next( - createHttpError( - HttpCode.BAD_REQUEST, - "An IDP with this ID does not exist." - ) + createHttpError(HttpCode.NOT_FOUND, "Idp does not exist") ); } if (existing.idpOrg) { return next( createHttpError( - HttpCode.BAD_REQUEST, - "An IDP org policy already exists." + HttpCode.CONFLICT, + "Org policy already exists for this idp" ) ); } @@ -109,15 +100,15 @@ export async function createIdpOrgPolicy( await db.insert(idpOrg).values({ idpId, orgId, - roleMapping, - orgMapping + orgMapping, + roleMapping }); return response(res, { data: {}, success: true, error: false, - message: "Idp created successfully", + message: "Idp org policy created successfully", status: HttpCode.CREATED }); } catch (error) { diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index d663afe..e7fc6a5 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -11,7 +11,6 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -import license from "@server/license/license"; const paramsSchema = z.object({}).strict(); @@ -81,10 +80,6 @@ export async function createOidcIdp( autoProvision } = parsedBody.data; - if (!(await license.isUnlocked())) { - autoProvision = false; - } - const key = config.getRawConfig().server.secret; const encryptedSecret = encrypt(clientSecret, key); diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index 5c41c95..51b8255 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -1,8 +1,3 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; @@ -11,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOrg } from "@server/db/schemas"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; @@ -25,7 +20,7 @@ const paramsSchema = z registry.registerPath({ method: "delete", path: "/idp/{idpId}/org/{orgId}", - description: "Create an OIDC IdP for an organization.", + description: "Delete an IDP policy for an IDP on an organization.", tags: [OpenAPITags.Idp], request: { params: paramsSchema @@ -51,26 +46,27 @@ export async function deleteIdpOrgPolicy( const { idpId, orgId } = parsedParams.data; + // Check if IDP policy, exists const [existing] = await db .select() .from(idp) - .leftJoin(idpOrg, eq(idpOrg.orgId, orgId)) - .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))); + .leftJoin( + idpOrg, + and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) + ) + .where(eq(idp.idpId, idpId)); - if (!existing.idp) { + if (!existing?.idp) { return next( - createHttpError( - HttpCode.BAD_REQUEST, - "An IDP with this ID does not exist." - ) + createHttpError(HttpCode.NOT_FOUND, "Idp does not exist") ); } if (!existing.idpOrg) { return next( createHttpError( - HttpCode.BAD_REQUEST, - "A policy for this IDP and org does not exist." + HttpCode.NOT_FOUND, + "Org policy does not exist for this idp" ) ); } @@ -83,7 +79,7 @@ export async function deleteIdpOrgPolicy( data: null, success: true, error: false, - message: "Policy deleted successfully", + message: "Idp policy deleted successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index f0dcf02..3749138 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -7,5 +7,5 @@ export * from "./validateOidcCallback"; export * from "./getIdp"; export * from "./createIdpOrgPolicy"; export * from "./deleteIdpOrgPolicy"; -export * from "./listIdpOrgPolicies"; export * from "./updateIdpOrgPolicy"; +export * from "./listIdpOrgPolicies"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts index 9ff9c97..b2105f4 100644 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -1,12 +1,7 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { idpOrg } from "@server/db/schemas"; +import { idpOrg, type IdpOrg } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -15,9 +10,11 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const paramsSchema = z.object({ - idpId: z.coerce.number() -}); +const paramsSchema = z + .object({ + idpId: z.coerce.number() + }) + .strict(); const querySchema = z .object({ @@ -47,8 +44,12 @@ async function query(idpId: number, limit: number, offset: number) { } export type ListIdpOrgPoliciesResponse = { - policies: NonNullable>>; - pagination: { total: number; limit: number; offset: number }; + policies: Array; + pagination: { + total: number; + limit: number; + offset: number; + }; }; registry.registerPath({ @@ -78,6 +79,7 @@ export async function listIdpOrgPolicies( ) ); } + const { idpId } = parsedParams.data; const parsedQuery = querySchema.safeParse(req.query); @@ -109,7 +111,7 @@ export async function listIdpOrgPolicies( }, success: true, error: false, - message: "Policies retrieved successfully", + message: "Idp org policies retrieved successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/idp/oidcAutoProvision.ts b/server/routers/idp/oidcAutoProvision.ts index 7861fc4..2687369 100644 --- a/server/routers/idp/oidcAutoProvision.ts +++ b/server/routers/idp/oidcAutoProvision.ts @@ -1,21 +1,27 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - +import { Request, Response } from "express"; +import { z } from "zod"; import { createSession, generateId, generateSessionToken, serializeSessionCookie } from "@server/auth/sessions/app"; -import db from "@server/db"; -import { Idp, idpOrg, orgs, roles, User, userOrgs, users } from "@server/db/schemas"; import logger from "@server/logger"; -import { UserType } from "@server/types/UserTypes"; +import db from "@server/db"; +import { + Idp, + idpOrg, + orgs, + roles, + User, + userOrgs, + users +} from "@server/db/schemas"; import { eq, and, inArray } from "drizzle-orm"; import jmespath from "jmespath"; -import { Request, Response } from "express"; +import { UserType } from "@server/types/UserTypes"; + +const extractedRolesSchema = z.array(z.string()).or(z.string()).nullable(); export async function oidcAutoProvision({ idp, @@ -36,83 +42,92 @@ export async function oidcAutoProvision({ req: Request; res: Response; }) { + // Get user's roles of all orgs as stated in the ID token claims const allOrgs = await db.select().from(orgs); + const userOrgInfo: { orgId: string; roleId: number }[] = []; - const defaultRoleMapping = idp.defaultRoleMapping; - const defaultOrgMapping = idp.defaultOrgMapping; - - let userOrgInfo: { orgId: string; roleId: number }[] = []; for (const org of allOrgs) { - const [idpOrgRes] = await db + const idpOrgs = await db .select() .from(idpOrg) .where( and(eq(idpOrg.idpId, idp.idpId), eq(idpOrg.orgId, org.orgId)) ); + if (idpOrgs.length === 0) continue; + const idpOrgRes = idpOrgs[0]; - let roleId: number | undefined = undefined; + const orgMapping = hydrateOrgMapping( + idpOrgRes.orgMapping || idp.defaultOrgMapping, + org.orgId + ); + const roleMapping = idpOrgRes.roleMapping || idp.defaultRoleMapping; - const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping; - const hydratedOrgMapping = hydrateOrgMapping(orgMapping, org.orgId); - - if (hydratedOrgMapping) { - logger.debug("Hydrated Org Mapping", { - hydratedOrgMapping - }); - const orgId = jmespath.search(claims, hydratedOrgMapping); - logger.debug("Extraced Org ID", { orgId }); + if (orgMapping) { + const orgId = jmespath.search(claims, orgMapping); + logger.debug("Extracted org ID", { orgId }); if (orgId !== true && orgId !== org.orgId) { // user not allowed to access this org continue; } } - const roleMapping = idpOrgRes?.roleMapping || defaultRoleMapping; if (roleMapping) { - logger.debug("Role Mapping", { roleMapping }); - const roleName = jmespath.search(claims, roleMapping); - - if (!roleName) { - logger.error("Role name not found in the ID token", { - roleName + logger.info("claims", { claims }); + const extractedRoles = extractedRolesSchema.safeParse( + jmespath.search(claims, roleMapping) + ); + if (!extractedRoles.success) { + logger.error("Error extracting roles", { + error: extractedRoles.error }); continue; } + const rd = extractedRoles.data; + if (!rd) { + continue; + } + const rolesFromToken = typeof rd === "string" ? [rd] : rd; + logger.debug("Extracted roles", { rolesFromToken }); + if (rd.length === 0) { + continue; + } - const [roleRes] = await db + const rolesFromDb = await db .select() .from(roles) .where( - and(eq(roles.orgId, org.orgId), eq(roles.name, roleName)) + and( + eq(roles.orgId, org.orgId), + inArray(roles.name, rolesFromToken) + ) ); - - if (!roleRes) { - logger.error("Role not found", { - orgId: org.orgId, - roleName - }); + if (rolesFromDb.length === 0) { + logger.error("Role(s) not found", { roles: rolesFromToken }); continue; } + if (rolesFromDb.length < rolesFromToken.length) { + logger.warn("Role(s) not found", { + roles: rolesFromToken.filter( + (r) => !rolesFromDb.some((rdb) => rdb.name === r) + ) + }); + } - roleId = roleRes.roleId; - - userOrgInfo.push({ - orgId: org.orgId, - roleId + rolesFromDb.forEach((r) => { + userOrgInfo.push({ + orgId: org.orgId, + roleId: r.roleId + }); }); } } - logger.debug("User org info", { userOrgInfo }); - let existingUserId = existingUser?.userId; - + let userId = existingUser?.userId; // sync the user with the orgs and roles await db.transaction(async (trx) => { - let userId = existingUser?.userId; - - // create user if not exists - if (!existingUser) { + if (!userId) { + // create user if it does not exist userId = generateId(15); await trx.insert(users).values({ @@ -126,7 +141,7 @@ export async function oidcAutoProvision({ dateCreated: new Date().toISOString() }); } else { - // set the name and email + // update username/email await trx .update(users) .set({ @@ -134,84 +149,60 @@ export async function oidcAutoProvision({ email: email || null, name: name || null }) - .where(eq(users.userId, userId!)); + .where(eq(users.userId, userId)); } - existingUserId = userId; - - // get all current user orgs + // get all current user orgs/roles const currentUserOrgs = await trx .select() .from(userOrgs) - .where(eq(userOrgs.userId, userId!)); + .where(eq(userOrgs.userId, userId)); // Delete orgs that are no longer valid - const orgsToDelete = currentUserOrgs.filter( - (currentOrg) => - !userOrgInfo.some((newOrg) => newOrg.orgId === currentOrg.orgId) - ); + const orgsToDelete = currentUserOrgs + .filter( + (currentOrg) => + !userOrgInfo.some( + (newOrg) => + newOrg.orgId === currentOrg.orgId && + newOrg.roleId === currentOrg.roleId + ) + ) + .map((org) => org.orgId); if (orgsToDelete.length > 0) { - await trx.delete(userOrgs).where( - and( - eq(userOrgs.userId, userId!), - inArray( - userOrgs.orgId, - orgsToDelete.map((org) => org.orgId) + await trx + .delete(userOrgs) + .where( + and( + eq(userOrgs.userId, userId!), + inArray(userOrgs.orgId, orgsToDelete) ) - ) - ); - } - - // Update roles for existing orgs where the role has changed - const orgsToUpdate = currentUserOrgs.filter((currentOrg) => { - const newOrg = userOrgInfo.find( - (newOrg) => newOrg.orgId === currentOrg.orgId - ); - return newOrg && newOrg.roleId !== currentOrg.roleId; - }); - - if (orgsToUpdate.length > 0) { - for (const org of orgsToUpdate) { - const newRole = userOrgInfo.find( - (newOrg) => newOrg.orgId === org.orgId ); - if (newRole) { - await trx - .update(userOrgs) - .set({ roleId: newRole.roleId }) - .where( - and( - eq(userOrgs.userId, userId!), - eq(userOrgs.orgId, org.orgId) - ) - ); - } - } } // Add new orgs that don't exist yet const orgsToAdd = userOrgInfo.filter( (newOrg) => !currentUserOrgs.some( - (currentOrg) => currentOrg.orgId === newOrg.orgId + (currentOrg) => + currentOrg.orgId === newOrg.orgId && + currentOrg.roleId === newOrg.roleId ) ); - if (orgsToAdd.length > 0) { await trx.insert(userOrgs).values( orgsToAdd.map((org) => ({ userId: userId!, orgId: org.orgId, - roleId: org.roleId, - dateCreated: new Date().toISOString() + roleId: org.roleId })) ); } }); const token = generateSessionToken(); - const sess = await createSession(token, existingUserId!); + const sess = await createSession(token, userId!); const isSecure = req.protocol === "https"; const cookie = serializeSessionCookie( token, @@ -225,9 +216,9 @@ export async function oidcAutoProvision({ function hydrateOrgMapping( orgMapping: string | null, orgId: string -): string | undefined { +): string | null { if (!orgMapping) { - return undefined; + return null; } - return orgMapping.split("{{orgId}}").join(orgId); + return orgMapping.replaceAll("{{orgId}}", orgId); } diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts index 6f8580a..642837d 100644 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -1,8 +1,3 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; @@ -12,8 +7,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { eq, and } from "drizzle-orm"; import { idp, idpOrg } from "@server/db/schemas"; +import { eq, and } from "drizzle-orm"; const paramsSchema = z .object({ @@ -64,6 +59,7 @@ export async function updateIdpOrgPolicy( ) ); } + const { idpId, orgId } = parsedParams.data; const parsedBody = bodySchema.safeParse(req.body); if (!parsedBody.success) { @@ -74,11 +70,9 @@ export async function updateIdpOrgPolicy( ) ); } + let { orgMapping, roleMapping } = parsedBody.data; - const { idpId, orgId } = parsedParams.data; - const { roleMapping, orgMapping } = parsedBody.data; - - // Check if IDP and policy exist + // Given identity provider must exist and have a policy already const [existing] = await db .select() .from(idp) @@ -90,36 +84,36 @@ export async function updateIdpOrgPolicy( if (!existing?.idp) { return next( - createHttpError( - HttpCode.BAD_REQUEST, - "An IDP with this ID does not exist." - ) + createHttpError(HttpCode.NOT_FOUND, "Idp does not exist") ); } if (!existing.idpOrg) { return next( createHttpError( - HttpCode.BAD_REQUEST, - "A policy for this IDP and org does not exist." + HttpCode.NOT_FOUND, + "Org policy does not exist for this idp" ) ); } - // Update the policy await db .update(idpOrg) .set({ - roleMapping, - orgMapping + idpId, + orgId, + orgMapping, + roleMapping }) - .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); + .where(and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId))); return response(res, { - data: {}, + data: { + idpId + }, success: true, error: false, - message: "Policy updated successfully", + message: "Idp org policy updated successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index d24e319..49a16a5 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -11,7 +11,6 @@ import { idp, idpOidcConfig } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -import license from "@server/license/license"; const paramsSchema = z .object({ @@ -100,10 +99,6 @@ export async function updateOidcIdp( defaultOrgMapping } = parsedBody.data; - if (!(await license.isUnlocked())) { - autoProvision = false; - } - // Check if IDP exists and is of type OIDC const [existingIdp] = await db .select() diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 7d588fe..274350d 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -20,7 +20,6 @@ import { } from "@server/auth/sessions/app"; import { decrypt } from "@server/lib/crypto"; import { oidcAutoProvision } from "./oidcAutoProvision"; -import license from "@server/license/license"; const ensureTrailingSlash = (url: string): string => { return url; @@ -210,14 +209,6 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - if (!(await license.isUnlocked())) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Auto-provisioning is not available" - ) - ); - } await oidcAutoProvision({ idp: existingIdp.idp, userIdentifier, diff --git a/server/routers/integration.ts b/server/routers/integration.ts deleted file mode 100644 index 40ab9aa..0000000 --- a/server/routers/integration.ts +++ /dev/null @@ -1,499 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import * as site from "./site"; -import * as org from "./org"; -import * as resource from "./resource"; -import * as domain from "./domain"; -import * as target from "./target"; -import * as user from "./user"; -import * as role from "./role"; -// import * as client from "./client"; -import * as accessToken from "./accessToken"; -import * as apiKeys from "./apiKeys"; -import * as idp from "./idp"; -import { - verifyApiKey, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction, - verifyApiKeySiteAccess, - verifyApiKeyResourceAccess, - verifyApiKeyTargetAccess, - verifyApiKeyRoleAccess, - verifyApiKeyUserAccess, - verifyApiKeySetResourceUsers, - verifyApiKeyAccessTokenAccess, - verifyApiKeyIsRoot -} from "@server/middlewares"; -import HttpCode from "@server/types/HttpCode"; -import { Router } from "express"; -import { ActionsEnum } from "@server/auth/actions"; - -export const unauthenticated = Router(); - -unauthenticated.get("/", (_, res) => { - res.status(HttpCode.OK).json({ message: "Healthy" }); -}); - -export const authenticated = Router(); -authenticated.use(verifyApiKey); - -authenticated.get( - "/org/checkId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.checkOrgId), - org.checkId -); - -authenticated.put( - "/org", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createOrg), - org.createOrg -); - -authenticated.get( - "/orgs", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listOrgs), - org.listOrgs -); // TODO we need to check the orgs here - -authenticated.get( - "/org/:orgId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.getOrg), - org.getOrg -); - -authenticated.post( - "/org/:orgId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.updateOrg), - org.updateOrg -); - -authenticated.delete( - "/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteOrg), - org.deleteOrg -); - -authenticated.put( - "/org/:orgId/site", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createSite), - site.createSite -); - -authenticated.get( - "/org/:orgId/sites", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listSites), - site.listSites -); - -authenticated.get( - "/org/:orgId/site/:niceId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.getSite), - site.getSite -); - -authenticated.get( - "/org/:orgId/pick-site-defaults", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createSite), - site.pickSiteDefaults -); - -authenticated.get( - "/site/:siteId", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.getSite), - site.getSite -); - -authenticated.post( - "/site/:siteId", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.updateSite), - site.updateSite -); - -authenticated.delete( - "/site/:siteId", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.deleteSite), - site.deleteSite -); - -authenticated.put( - "/org/:orgId/site/:siteId/resource", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createResource), - resource.createResource -); - -authenticated.get( - "/site/:siteId/resources", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.listResources), - resource.listResources -); - -authenticated.get( - "/org/:orgId/resources", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listResources), - resource.listResources -); - -authenticated.get( - "/org/:orgId/domains", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listOrgDomains), - domain.listDomains -); - -authenticated.post( - "/org/:orgId/create-invite", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.inviteUser), - user.inviteUser -); - -authenticated.get( - "/resource/:resourceId/roles", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceRoles), - resource.listResourceRoles -); - -authenticated.get( - "/resource/:resourceId/users", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceUsers), - resource.listResourceUsers -); - -authenticated.get( - "/resource/:resourceId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.getResource), - resource.getResource -); - -authenticated.post( - "/resource/:resourceId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResource), - resource.updateResource -); - -authenticated.delete( - "/resource/:resourceId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.deleteResource), - resource.deleteResource -); - -authenticated.put( - "/resource/:resourceId/target", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.createTarget), - target.createTarget -); - -authenticated.get( - "/resource/:resourceId/targets", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listTargets), - target.listTargets -); - -authenticated.put( - "/resource/:resourceId/rule", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.createResourceRule), - resource.createResourceRule -); - -authenticated.get( - "/resource/:resourceId/rules", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceRules), - resource.listResourceRules -); - -authenticated.post( - "/resource/:resourceId/rule/:ruleId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResourceRule), - resource.updateResourceRule -); - -authenticated.delete( - "/resource/:resourceId/rule/:ruleId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.deleteResourceRule), - resource.deleteResourceRule -); - -authenticated.get( - "/target/:targetId", - verifyApiKeyTargetAccess, - verifyApiKeyHasAction(ActionsEnum.getTarget), - target.getTarget -); - -authenticated.post( - "/target/:targetId", - verifyApiKeyTargetAccess, - verifyApiKeyHasAction(ActionsEnum.updateTarget), - target.updateTarget -); - -authenticated.delete( - "/target/:targetId", - verifyApiKeyTargetAccess, - verifyApiKeyHasAction(ActionsEnum.deleteTarget), - target.deleteTarget -); - -authenticated.put( - "/org/:orgId/role", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createRole), - role.createRole -); - -authenticated.get( - "/org/:orgId/roles", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listRoles), - role.listRoles -); - -authenticated.delete( - "/role/:roleId", - verifyApiKeyRoleAccess, - verifyApiKeyHasAction(ActionsEnum.deleteRole), - role.deleteRole -); - -authenticated.post( - "/role/:roleId/add/:userId", - verifyApiKeyRoleAccess, - verifyApiKeyUserAccess, - verifyApiKeyHasAction(ActionsEnum.addUserRole), - user.addUserRole -); - -authenticated.post( - "/resource/:resourceId/roles", - verifyApiKeyResourceAccess, - verifyApiKeyRoleAccess, - verifyApiKeyHasAction(ActionsEnum.setResourceRoles), - resource.setResourceRoles -); - -authenticated.post( - "/resource/:resourceId/users", - verifyApiKeyResourceAccess, - verifyApiKeySetResourceUsers, - verifyApiKeyHasAction(ActionsEnum.setResourceUsers), - resource.setResourceUsers -); - -authenticated.post( - `/resource/:resourceId/password`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourcePassword), - resource.setResourcePassword -); - -authenticated.post( - `/resource/:resourceId/pincode`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourcePincode), - resource.setResourcePincode -); - -authenticated.post( - `/resource/:resourceId/whitelist`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), - resource.setResourceWhitelist -); - -authenticated.get( - `/resource/:resourceId/whitelist`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist), - resource.getResourceWhitelist -); - -authenticated.post( - `/resource/:resourceId/transfer`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResource), - resource.transferResource -); - -authenticated.post( - `/resource/:resourceId/access-token`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.generateAccessToken), - accessToken.generateAccessToken -); - -authenticated.delete( - `/access-token/:accessTokenId`, - verifyApiKeyAccessTokenAccess, - verifyApiKeyHasAction(ActionsEnum.deleteAcessToken), - accessToken.deleteAccessToken -); - -authenticated.get( - `/org/:orgId/access-tokens`, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listAccessTokens), - accessToken.listAccessTokens -); - -authenticated.get( - `/resource/:resourceId/access-tokens`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listAccessTokens), - accessToken.listAccessTokens -); - -authenticated.get( - "/org/:orgId/user/:userId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.getOrgUser), - user.getOrgUser -); - -authenticated.get( - "/org/:orgId/users", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listUsers), - user.listUsers -); - -authenticated.delete( - "/org/:orgId/user/:userId", - verifyApiKeyOrgAccess, - verifyApiKeyUserAccess, - verifyApiKeyHasAction(ActionsEnum.removeUser), - user.removeUserOrg -); - -// authenticated.put( -// "/newt", -// verifyApiKeyHasAction(ActionsEnum.createNewt), -// newt.createNewt -// ); - -authenticated.get( - `/org/:orgId/api-keys`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listApiKeys), - apiKeys.listOrgApiKeys -); - -authenticated.post( - `/org/:orgId/api-key/:apiKeyId/actions`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.setApiKeyActions), - apiKeys.setApiKeyActions -); - -authenticated.get( - `/org/:orgId/api-key/:apiKeyId/actions`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listApiKeyActions), - apiKeys.listApiKeyActions -); - -authenticated.put( - `/org/:orgId/api-key`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createApiKey), - apiKeys.createOrgApiKey -); - -authenticated.delete( - `/org/:orgId/api-key/:apiKeyId`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteApiKey), - apiKeys.deleteApiKey -); - -authenticated.put( - "/idp/oidc", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createIdp), - idp.createOidcIdp -); - -authenticated.post( - "/idp/:idpId/oidc", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.updateIdp), - idp.updateOidcIdp -); - -authenticated.delete( - "/idp/:idpId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteIdp), - idp.deleteIdp -); - -authenticated.get( - "/idp", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listIdps), - idp.listIdps -); - -authenticated.get( - "/idp/:idpId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.getIdp), - idp.getIdp -); - -authenticated.put( - "/idp/:idpId/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createIdpOrg), - idp.createIdpOrgPolicy -); - -authenticated.post( - "/idp/:idpId/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.updateIdpOrg), - idp.updateIdpOrgPolicy -); - -authenticated.delete( - "/idp/:idpId/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg), - idp.deleteIdpOrgPolicy -); - -authenticated.get( - "/idp/:idpId/org", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listIdpOrgs), - idp.listIdpOrgPolicies -); diff --git a/server/routers/internal.ts b/server/routers/internal.ts index eee72e9..fbc3f9e 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -4,8 +4,6 @@ import * as traefik from "@server/routers/traefik"; import * as resource from "./resource"; import * as badger from "./badger"; import * as auth from "@server/routers/auth"; -import * as supporterKey from "@server/routers/supporterKey"; -import * as license from "@server/routers/license"; import HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, @@ -33,16 +31,6 @@ internalRouter.post( resource.getExchangeToken ); -internalRouter.get( - `/supporter-key/visible`, - supporterKey.isSupporterKeyVisible -); - -internalRouter.get( - `/license/status`, - license.getLicenseStatus -); - // Gerbil routes const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); diff --git a/server/routers/license/activateLicense.ts b/server/routers/license/activateLicense.ts deleted file mode 100644 index da2b76c..0000000 --- a/server/routers/license/activateLicense.ts +++ /dev/null @@ -1,62 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import license, { LicenseStatus } from "@server/license/license"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; - -const bodySchema = z - .object({ - licenseKey: z.string().min(1).max(255) - }) - .strict(); - -export type ActivateLicenseStatus = LicenseStatus; - -export async function activateLicense( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/license/deleteLicenseKey.ts b/server/routers/license/deleteLicenseKey.ts deleted file mode 100644 index bea7f9a..0000000 --- a/server/routers/license/deleteLicenseKey.ts +++ /dev/null @@ -1,78 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import db from "@server/db"; -import { eq } from "drizzle-orm"; -import { licenseKey } from "@server/db/schemas"; -import license, { LicenseStatus } from "@server/license/license"; -import { encrypt } from "@server/lib/crypto"; -import config from "@server/lib/config"; - -const paramsSchema = z - .object({ - licenseKey: z.string().min(1).max(255) - }) - .strict(); - -export type DeleteLicenseKeyResponse = LicenseStatus; - -export async function deleteLicenseKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/license/getLicenseStatus.ts b/server/routers/license/getLicenseStatus.ts deleted file mode 100644 index a4e4151..0000000 --- a/server/routers/license/getLicenseStatus.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import license, { LicenseStatus } from "@server/license/license"; - -export type GetLicenseStatusResponse = LicenseStatus; - -export async function getLicenseStatus( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const status = await license.check(); - - return sendResponse(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") - ); - } -} diff --git a/server/routers/license/index.ts b/server/routers/license/index.ts deleted file mode 100644 index 6c848c2..0000000 --- a/server/routers/license/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -export * from "./getLicenseStatus"; -export * from "./activateLicense"; -export * from "./listLicenseKeys"; -export * from "./deleteLicenseKey"; -export * from "./recheckStatus"; diff --git a/server/routers/license/listLicenseKeys.ts b/server/routers/license/listLicenseKeys.ts deleted file mode 100644 index 12a1956..0000000 --- a/server/routers/license/listLicenseKeys.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import license, { LicenseKeyCache } from "@server/license/license"; - -export type ListLicenseKeysResponse = LicenseKeyCache[]; - -export async function listLicenseKeys( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const keys = license.listKeys(); - - return sendResponse(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") - ); - } -} diff --git a/server/routers/license/recheckStatus.ts b/server/routers/license/recheckStatus.ts deleted file mode 100644 index 5f0bd94..0000000 --- a/server/routers/license/recheckStatus.ts +++ /dev/null @@ -1,42 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import license, { LicenseStatus } from "@server/license/license"; - -export type RecheckStatusResponse = LicenseStatus; - -export async function recheckStatus( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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") - ); - } -} diff --git a/server/routers/newt/createNewt.ts b/server/routers/newt/createNewt.ts index 02517db..b69ada3 100644 --- a/server/routers/newt/createNewt.ts +++ b/server/routers/newt/createNewt.ts @@ -49,7 +49,7 @@ export async function createNewt( const { newtId, secret } = parsedBody.data; - if (req.user && !req.userOrgRoleId) { + if (req.user && !req.userRoleIds) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); diff --git a/server/routers/org/getOrgOverview.ts b/server/routers/org/getOrgOverview.ts index dcde292..59ae08f 100644 --- a/server/routers/org/getOrgOverview.ts +++ b/server/routers/org/getOrgOverview.ts @@ -11,12 +11,13 @@ import { users, userSites } from "@server/db/schemas"; -import { and, count, eq, inArray } from "drizzle-orm"; +import { and, count, eq, inArray, countDistinct } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; +import { RoleItem } from "../user/getOrgUser"; const getOrgParamsSchema = z .object({ @@ -27,7 +28,7 @@ const getOrgParamsSchema = z export type GetOrgOverviewResponse = { orgName: string; orgId: string; - userRoleName: string; + roles: RoleItem[]; numSites: number; numUsers: number; numResources: number; @@ -115,24 +116,25 @@ export async function getOrgOverview( ); const [{ numUsers }] = await db - .select({ numUsers: count() }) + .select({ numUsers: countDistinct(userOrgs.userId) }) .from(userOrgs) .where(eq(userOrgs.orgId, orgId)); - const [role] = await db - .select() + const userRoles = await db + .select({ id: roles.roleId, name: roles.name }) .from(roles) - .where(eq(roles.roleId, req.userOrg.roleId)); + .where(inArray(roles.roleId, req.userRoleIds ?? [])) + .orderBy(roles.name); return response(res, { data: { orgName: org[0].name, orgId: org[0].orgId, - userRoleName: role.name, + roles: userRoles, numSites, numUsers, numResources, - isAdmin: role.name === "Admin", + isAdmin: userRoles.some((r) => r.name === "Admin"), isOwner: req.userOrg?.isOwner || false }, success: true, diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index e899530..35dc4bf 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -130,7 +130,7 @@ export async function createResource( const { siteId, orgId } = parsedParams.data; - if (req.user && !req.userOrgRoleId) { + if (req.user && !req.userRoleIds) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); @@ -285,7 +285,7 @@ async function createHttpResource( resourceId: newResource[0].resourceId }); - if (req.user && req.userOrgRoleId != adminRole[0].roleId) { + if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { // make sure the user can access the resource await trx.insert(userResources).values({ userId: req.user?.userId!, @@ -392,7 +392,7 @@ async function createRawResource( resourceId: newResource[0].resourceId }); - if (req.userOrgRoleId != adminRole[0].roleId) { + if (req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { // make sure the user can access the resource await trx.insert(userResources).values({ userId: req.user?.userId!, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 9af2474..49de7aa 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -216,7 +216,7 @@ export async function listResources( .where( or( eq(userResources.userId, req.user!.userId), - eq(roleResources.roleId, req.userOrgRoleId!) + inArray(roleResources.roleId, req.userRoleIds!) ) ); } else { diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 87eaa95..a4444b8 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -99,7 +99,7 @@ export async function createSite( const { orgId } = parsedParams.data; - if (req.user && !req.userOrgRoleId) { + if (req.user && !req.userRoleIds) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); @@ -176,7 +176,7 @@ export async function createSite( siteId: newSite.siteId }); - if (req.user && req.userOrgRoleId != adminRole[0].roleId) { + if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { // make sure the user can access the site trx.insert(userSites).values({ userId: req.user?.userId!, diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 1b8791c..8dde88f 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -120,7 +120,7 @@ export async function listSites( .where( or( eq(userSites.userId, req.user!.userId), - eq(roleSites.roleId, req.userOrgRoleId!) + inArray(roleSites.roleId, req.userRoleIds!) ) ); } else { diff --git a/server/routers/supporterKey/hideSupporterKey.ts b/server/routers/supporterKey/hideSupporterKey.ts deleted file mode 100644 index f9d4e89..0000000 --- a/server/routers/supporterKey/hideSupporterKey.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import config from "@server/lib/config"; - -export type HideSupporterKeyResponse = { - hidden: boolean; -}; - -export async function hideSupporterKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - config.hideSupporterKey(); - - return sendResponse(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") - ); - } -} diff --git a/server/routers/supporterKey/index.ts b/server/routers/supporterKey/index.ts deleted file mode 100644 index 4e339a6..0000000 --- a/server/routers/supporterKey/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./validateSupporterKey"; -export * from "./isSupporterKeyVisible"; -export * from "./hideSupporterKey"; diff --git a/server/routers/supporterKey/isSupporterKeyVisible.ts b/server/routers/supporterKey/isSupporterKeyVisible.ts deleted file mode 100644 index 15e313d..0000000 --- a/server/routers/supporterKey/isSupporterKeyVisible.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { response as sendResponse } from "@server/lib"; -import config from "@server/lib/config"; -import db from "@server/db"; -import { count } from "drizzle-orm"; -import { users } from "@server/db/schemas"; -import license from "@server/license/license"; - -export type IsSupporterKeyVisibleResponse = { - visible: boolean; - tier?: string; -}; - -const USER_LIMIT = 5; - -export async function isSupporterKeyVisible( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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") - ); - } -} diff --git a/server/routers/supporterKey/validateSupporterKey.ts b/server/routers/supporterKey/validateSupporterKey.ts deleted file mode 100644 index fadcdc3..0000000 --- a/server/routers/supporterKey/validateSupporterKey.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { response as sendResponse } from "@server/lib"; -import { suppressDeprecationWarnings } from "moment"; -import { supporterKey } from "@server/db/schemas"; -import db from "@server/db"; -import { eq } from "drizzle-orm"; -import config from "@server/lib/config"; - -const validateSupporterKeySchema = z - .object({ - githubUsername: z.string().nonempty(), - key: z.string().nonempty() - }) - .strict(); - -export type ValidateSupporterKeyResponse = { - valid: boolean; - githubUsername?: string; - tier?: string; - phrase?: string; -}; - -export async function validateSupporterKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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(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") - ); - } -} diff --git a/server/routers/user/addUserRole.ts b/server/routers/user/addUserRole.ts index c0ac31b..b1c9025 100644 --- a/server/routers/user/addUserRole.ts +++ b/server/routers/user/addUserRole.ts @@ -105,14 +105,26 @@ export async function addUserRole( ); } - const newUserRole = await db - .update(userOrgs) - .set({ roleId }) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) - .returning(); + const newUserRole = { orgId, userId, roleId, isOwner: false }; + + await db.transaction(async (trx) => { + const hasRoleAlready = await trx + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, orgId), + eq(userOrgs.roleId, roleId) + ) + ); + if (hasRoleAlready.length === 0) { + await trx.insert(userOrgs).values(newUserRole); + } + }); return response(res, { - data: newUserRole[0], + data: newUserRole, success: true, error: false, message: "Role added to user successfully", diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 6ebd33c..226248a 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { roles, userOrgs, users } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -10,6 +10,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import { OpenAPITags, registry } from "@server/openApi"; +import { StringDecoder } from "string_decoder"; async function queryUser(orgId: string, userId: string) { const [user] = await db @@ -20,8 +21,7 @@ async function queryUser(orgId: string, userId: string) { username: users.username, name: users.name, type: users.type, - roleId: userOrgs.roleId, - roleName: roles.name, + roles: sql`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`, isOwner: userOrgs.isOwner, isAdmin: roles.isAdmin }) @@ -30,9 +30,17 @@ async function queryUser(orgId: string, userId: string) { .leftJoin(users, eq(userOrgs.userId, users.userId)) .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .limit(1); + if (typeof user.roles === "string") { + user.roles = JSON.parse(user.roles); + } return user; } +export type RoleItem = { + id: number; + name: string; +}; + export type GetOrgUserResponse = NonNullable< Awaited> >; diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 49278c1..a9400cd 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -2,6 +2,7 @@ export * from "./getUser"; export * from "./removeUserOrg"; export * from "./listUsers"; export * from "./addUserRole"; +export * from "./setUserRoles"; export * from "./inviteUser"; export * from "./acceptInvite"; export * from "./getOrgUser"; diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index fd2291d..89752eb 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -5,11 +5,11 @@ import { idp, roles, userOrgs, users } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { and, sql } from "drizzle-orm"; +import { AnyColumn, eq, InferColumnsDataTypes, sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { eq } from "drizzle-orm"; +import { RoleItem } from "./getOrgUser"; const listUsersParamsSchema = z .object({ @@ -34,8 +34,20 @@ const listUsersSchema = z }) .strict(); +function jsonAggBuildObject>(shape: T) { + const shapeString = Object.entries(shape) + .map(([key, value]) => { + return `'${key}', ${value}`; + }) + .join(","); + + return sql< + InferColumnsDataTypes[] + >`json_agg(json_build_object(${shapeString}))`; +} + async function queryUsers(orgId: string, limit: number, offset: number) { - return await db + const res = await db .select({ id: users.userId, email: users.email, @@ -45,8 +57,7 @@ async function queryUsers(orgId: string, limit: number, offset: number) { username: users.username, name: users.name, type: users.type, - roleId: userOrgs.roleId, - roleName: roles.name, + roles: sql`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`, isOwner: userOrgs.isOwner, idpName: idp.name, idpId: users.idpId @@ -56,8 +67,15 @@ async function queryUsers(orgId: string, limit: number, offset: number) { .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .leftJoin(idp, eq(users.idpId, idp.idpId)) .where(eq(userOrgs.orgId, orgId)) + .groupBy(users.userId) .limit(limit) .offset(offset); + res.forEach((itm) => { + if (typeof itm.roles === "string") { + itm.roles = JSON.parse(itm.roles); + } + }); + return res; } export type ListUsersResponse = { diff --git a/server/routers/user/setUserRoles.ts b/server/routers/user/setUserRoles.ts new file mode 100644 index 0000000..e89c989 --- /dev/null +++ b/server/routers/user/setUserRoles.ts @@ -0,0 +1,175 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { userOrgs, roles } from "@server/db/schemas"; +import { eq, and, inArray } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setUserRolesParamsSchema = z + .object({ + orgId: z.string(), + userId: z.string() + }) + .strict(); + +const setUserRolesBodySchema = z.object({ + roleIds: z.array(z.number().int()).min(1) +}); + +export type SetUserRolesResponse = z.infer; + +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 { + 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") + ); + } +} diff --git a/src/app/[orgId]/OrganizationLandingCard.tsx b/src/app/[orgId]/OrganizationLandingCard.tsx index baffc09..6bf0f57 100644 --- a/src/app/[orgId]/OrganizationLandingCard.tsx +++ b/src/app/[orgId]/OrganizationLandingCard.tsx @@ -10,7 +10,8 @@ import { CardFooter } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react"; +import { Users, Settings, Waypoints, Combine } from "lucide-react"; +import { RoleItem } from "@server/routers/user"; interface OrgStat { label: string; @@ -26,7 +27,7 @@ type OrganizationLandingCardProps = { resources: number; users: number; }; - userRole: string; + roles: RoleItem[]; isAdmin: boolean; isOwner: boolean; orgId: string; @@ -81,9 +82,21 @@ export default function OrganizationLandingCard( ))}
- Your role:{" "} + Your role + {orgData.overview.isOwner || + orgData.overview.isAdmin || + orgData.overview.roles.length === 1 + ? "" + : "s"} + :{" "} - {orgData.overview.isOwner ? "Owner" : orgData.overview.userRole} + {orgData.overview.isOwner + ? "Owner" + : orgData.overview.isAdmin + ? "Admin" + : orgData.overview.roles + .map((r) => r.name) + .join(", ")}
diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 5f91fb6..a9d7884 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -74,7 +74,7 @@ export default async function OrgPage(props: OrgPageProps) { }, isAdmin: overview.isAdmin, isOwner: overview.isOwner, - userRole: overview.userRoleName + roles: overview.roles }} /> diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/app/[orgId]/settings/access/users/UsersTable.tsx index 8036cc8..e723985 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersTable.tsx @@ -150,7 +150,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { column.toggleSorting(column.getIsSorted() === "asc") } > - Role + Roles ); diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index 002febc..ca327d6 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -8,17 +8,9 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; -import { InviteUserResponse } from "@server/routers/user"; +import { SetUserRolesResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -40,10 +32,18 @@ import { import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; const formSchema = z.object({ email: z.string().email({ message: "Please enter a valid email" }), - roleId: z.string().min(1, { message: "Please select a role" }) + roles: z + .array( + z.object({ + id: z.string(), + text: z.string() + }) + ) + .min(1, { message: "Please select a role" }) }); export default function AccessControlsPage() { @@ -54,13 +54,18 @@ export default function AccessControlsPage() { const { orgId } = useParams(); const [loading, setLoading] = useState(false); - const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); + const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( + [] + ); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { email: user.email!, - roleId: user.roleId?.toString() + roles: [] } }); @@ -81,13 +86,24 @@ export default function AccessControlsPage() { }); if (res?.status === 200) { - setRoles(res.data.data.roles); + setAllRoles( + res.data.data.roles.map((role) => ({ + id: role.roleId.toString(), + text: role.name + })) + ); } } fetchRoles(); - form.setValue("roleId", user.roleId.toString()); + form.setValue( + "roles", + user.roles.map((i) => ({ + id: i.id.toString(), + text: i.name + })) + ); }, []); async function onSubmit(values: z.infer) { @@ -95,8 +111,8 @@ export default function AccessControlsPage() { const res = await api .post< - AxiosResponse - >(`/role/${values.roleId}/add/${user.userId}`) + AxiosResponse + >(`/org/${user.orgId}/user/${user.userId}/roles`, { roleIds: values.roles.map((r) => parseInt(r.id)) }) .catch((e) => { toast({ variant: "destructive", @@ -140,30 +156,44 @@ export default function AccessControlsPage() { > ( - - Role - + + Roles + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={ + allRoles + } + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + )} diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index f82cfdb..4f1a150 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -78,7 +78,7 @@ export default async function UsersPage(props: UsersPageProps) { idpId: user.idpId, idpName: user.idpName || "Internal", status: "Confirmed", - role: user.isOwner ? "Owner" : user.roleName || "Member", + role: user.isOwner ? "Owner" : user.roles.map((r) => r.name).join(", ") || "Member", isOwner: user.isOwner || false }; }); diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx deleted file mode 100644 index 69fe717..0000000 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { DataTable } from "@app/components/ui/data-table"; -import { ColumnDef } from "@tanstack/react-table"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - addApiKey?: () => void; -} - -export function OrgApiKeysDataTable({ - addApiKey, - columns, - data -}: DataTableProps) { - return ( - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx deleted file mode 100644 index 89e4784..0000000 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx +++ /dev/null @@ -1,204 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { OrgApiKeysDataTable } from "./OrgApiKeysDataTable"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; -import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import { toast } from "@app/hooks/useToast"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import moment from "moment"; - -export type OrgApiKeyRow = { - id: string; - key: string; - name: string; - createdAt: string; -}; - -type OrgApiKeyTableProps = { - apiKeys: OrgApiKeyRow[]; - orgId: string; -}; - -export default function OrgApiKeysTable({ - apiKeys, - orgId -}: OrgApiKeyTableProps) { - const router = useRouter(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selected, setSelected] = useState(null); - const [rows, setRows] = useState(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[] = [ - { - id: "dots", - cell: ({ row }) => { - const apiKeyROw = row.original; - const router = useRouter(); - - return ( - - - - - - { - setSelected(apiKeyROw); - }} - > - View settings - - { - setSelected(apiKeyROw); - setIsDeleteModalOpen(true); - }} - > - Delete - - - - ); - } - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "key", - header: "Key", - cell: ({ row }) => { - const r = row.original; - return {r.key}; - } - }, - { - accessorKey: "createdAt", - header: "Created At", - cell: ({ row }) => { - const r = row.original; - return {moment(r.createdAt).format("lll")} ; - } - }, - { - id: "actions", - cell: ({ row }) => { - const r = row.original; - return ( -
- - - -
- ); - } - } - ]; - - return ( - <> - {selected && ( - { - setIsDeleteModalOpen(val); - setSelected(null); - }} - dialog={ -
-

- Are you sure you want to remove the API key{" "} - {selected?.name || selected?.id} from the - organization? -

- -

- - Once removed, the API key will no longer be - able to be used. - -

- -

- To confirm, please type the name of the API key - below. -

-
- } - buttonText="Confirm Delete API Key" - onConfirm={async () => deleteSite(selected!.id)} - string={selected.name} - title="Delete API Key" - /> - )} - - { - router.push(`/${orgId}/settings/api-keys/create`); - }} - /> - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx deleted file mode 100644 index a4c13c9..0000000 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { internal } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { SidebarSettings } from "@app/components/SidebarSettings"; -import Link from "next/link"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator -} from "@app/components/ui/breadcrumb"; -import { GetApiKeyResponse } from "@server/routers/apiKeys"; -import ApiKeyProvider from "@app/providers/ApiKeyProvider"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; - -interface SettingsLayoutProps { - children: React.ReactNode; - params: Promise<{ apiKeyId: string; orgId: string }>; -} - -export default async function SettingsLayout(props: SettingsLayoutProps) { - const params = await props.params; - - const { children } = props; - - let apiKey = null; - try { - const res = await internal.get>( - `/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 ( - <> - - - - {children} - - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx deleted file mode 100644 index 7df37cd..0000000 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { redirect } from "next/navigation"; - -export default async function ApiKeysPage(props: { - params: Promise<{ orgId: string; apiKeyId: string }>; -}) { - const params = await props.params; - redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`); -} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx deleted file mode 100644 index d1e6f51..0000000 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { Button } from "@app/components/ui/button"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { ListApiKeyActionsResponse } from "@server/routers/apiKeys"; -import { AxiosResponse } from "axios"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; - -export default function Page() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const { orgId, apiKeyId } = useParams(); - - const [loadingPage, setLoadingPage] = useState(true); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - const [loadingSavePermissions, setLoadingSavePermissions] = - useState(false); - - useEffect(() => { - async function load() { - setLoadingPage(true); - - const res = await api - .get< - AxiosResponse - >(`/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 && ( - - - - - Permissions - - - Determine what this API key can do - - - - - - - - - - - - )} - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/create/page.tsx b/src/app/[orgId]/settings/api-keys/create/page.tsx deleted file mode 100644 index 3ede2ac..0000000 --- a/src/app/[orgId]/settings/api-keys/create/page.tsx +++ /dev/null @@ -1,412 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import HeaderTitle from "@app/components/SettingsSectionTitle"; -import { z } from "zod"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Input } from "@app/components/ui/input"; -import { InfoIcon } from "lucide-react"; -import { Button } from "@app/components/ui/button"; -import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { AxiosResponse } from "axios"; -import { useParams, useRouter } from "next/navigation"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator -} from "@app/components/ui/breadcrumb"; -import Link from "next/link"; -import { - CreateOrgApiKeyBody, - CreateOrgApiKeyResponse -} from "@server/routers/apiKeys"; -import { ApiKey } from "@server/db/schemas"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import CopyToClipboard from "@app/components/CopyToClipboard"; -import moment from "moment"; -import CopyCodeBox from "@server/emails/templates/components/CopyCodeBox"; -import CopyTextBox from "@app/components/CopyTextBox"; -import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; - -const createFormSchema = z.object({ - name: z - .string() - .min(2, { - message: "Name must be at least 2 characters." - }) - .max(255, { - message: "Name must not be longer than 255 characters." - }) -}); - -type CreateFormValues = z.infer; - -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; - -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(null); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - - const form = useForm({ - resolver: zodResolver(createFormSchema), - defaultValues: { - name: "" - } - }); - - const copiedForm = useForm({ - 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 - >(`/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 ( - <> -
- - -
- - {!loadingPage && ( -
- - {!apiKey && ( - <> - - - - API Key Information - - - - -
- - ( - - - Name - - - - - - - )} - /> - - -
-
-
- - - - - Permissions - - - Determine what this API key can do - - - - - - - - )} - - {apiKey && ( - - - - Your API Key - - - - - - - Name - - - - - - - - Created - - - {moment( - apiKey.createdAt - ).format("lll")} - - - - - - - - Save Your API Key - - - You will only be able to see this - once. Make sure to copy it to a - secure place. - - - -

- Your API key is: -

- - - -
- - ( - -
- { - copiedForm.setValue( - "copied", - e as boolean - ); - }} - /> - -
- -
- )} - /> - - -
-
- )} -
- -
- {!apiKey && ( - - )} - {!apiKey && ( - - )} - - {apiKey && ( - - )} -
-
- )} - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx deleted file mode 100644 index ef1e3dd..0000000 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import OrgApiKeysTable, { OrgApiKeyRow } from "./OrgApiKeysTable"; -import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; - -type ApiKeyPageProps = { - params: Promise<{ orgId: string }>; -}; - -export const dynamic = "force-dynamic"; - -export default async function ApiKeysPage(props: ApiKeyPageProps) { - const params = await props.params; - let apiKeys: ListOrgApiKeysResponse["apiKeys"] = []; - try { - const res = await internal.get>( - `/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 ( - <> - - - - - ); -} diff --git a/src/app/admin/api-keys/ApiKeysDataTable.tsx b/src/app/admin/api-keys/ApiKeysDataTable.tsx deleted file mode 100644 index f65949a..0000000 --- a/src/app/admin/api-keys/ApiKeysDataTable.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { - ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, - getPaginationRowModel, - SortingState, - getSortedRowModel, - ColumnFiltersState, - getFilteredRowModel -} from "@tanstack/react-table"; - -import { - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableHeader, - TableRow -} from "@/components/ui/table"; -import { Button } from "@app/components/ui/button"; -import { useState } from "react"; -import { Input } from "@app/components/ui/input"; -import { DataTablePagination } from "@app/components/DataTablePagination"; -import { Plus, Search } from "lucide-react"; -import { DataTable } from "@app/components/ui/data-table"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - addApiKey?: () => void; -} - -export function ApiKeysDataTable({ - addApiKey, - columns, - data -}: DataTableProps) { - return ( - - ); -} diff --git a/src/app/admin/api-keys/ApiKeysTable.tsx b/src/app/admin/api-keys/ApiKeysTable.tsx deleted file mode 100644 index c44d43f..0000000 --- a/src/app/admin/api-keys/ApiKeysTable.tsx +++ /dev/null @@ -1,199 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; -import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import { toast } from "@app/hooks/useToast"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import moment from "moment"; -import { ApiKeysDataTable } from "./ApiKeysDataTable"; - -export type ApiKeyRow = { - id: string; - key: string; - name: string; - createdAt: string; -}; - -type ApiKeyTableProps = { - apiKeys: ApiKeyRow[]; -}; - -export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { - const router = useRouter(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selected, setSelected] = useState(null); - const [rows, setRows] = useState(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[] = [ - { - id: "dots", - cell: ({ row }) => { - const apiKeyROw = row.original; - const router = useRouter(); - - return ( - - - - - - { - setSelected(apiKeyROw); - }} - > - View settings - - { - setSelected(apiKeyROw); - setIsDeleteModalOpen(true); - }} - > - Delete - - - - ); - } - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "key", - header: "Key", - cell: ({ row }) => { - const r = row.original; - return {r.key}; - } - }, - { - accessorKey: "createdAt", - header: "Created At", - cell: ({ row }) => { - const r = row.original; - return {moment(r.createdAt).format("lll")} ; - } - }, - { - id: "actions", - cell: ({ row }) => { - const r = row.original; - return ( -
- - - -
- ); - } - } - ]; - - return ( - <> - {selected && ( - { - setIsDeleteModalOpen(val); - setSelected(null); - }} - dialog={ -
-

- Are you sure you want to remove the API key{" "} - {selected?.name || selected?.id}? -

- -

- - Once removed, the API key will no longer be - able to be used. - -

- -

- To confirm, please type the name of the API key - below. -

-
- } - buttonText="Confirm Delete API Key" - onConfirm={async () => deleteSite(selected!.id)} - string={selected.name} - title="Delete API Key" - /> - )} - - { - router.push(`/admin/api-keys/create`); - }} - /> - - ); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/layout.tsx b/src/app/admin/api-keys/[apiKeyId]/layout.tsx deleted file mode 100644 index be3147e..0000000 --- a/src/app/admin/api-keys/[apiKeyId]/layout.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { internal } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { SidebarSettings } from "@app/components/SidebarSettings"; -import Link from "next/link"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator -} from "@app/components/ui/breadcrumb"; -import { GetApiKeyResponse } from "@server/routers/apiKeys"; -import ApiKeyProvider from "@app/providers/ApiKeyProvider"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; - -interface SettingsLayoutProps { - children: React.ReactNode; - params: Promise<{ apiKeyId: string }>; -} - -export default async function SettingsLayout(props: SettingsLayoutProps) { - const params = await props.params; - - const { children } = props; - - let apiKey = null; - try { - const res = await internal.get>( - `/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 ( - <> - - - - {children} - - - ); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/page.tsx b/src/app/admin/api-keys/[apiKeyId]/page.tsx deleted file mode 100644 index b0e4c3e..0000000 --- a/src/app/admin/api-keys/[apiKeyId]/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { redirect } from "next/navigation"; - -export default async function ApiKeysPage(props: { - params: Promise<{ apiKeyId: string }>; -}) { - const params = await props.params; - redirect(`/admin/api-keys/${params.apiKeyId}/permissions`); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx deleted file mode 100644 index c468c13..0000000 --- a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { Button } from "@app/components/ui/button"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { ListApiKeyActionsResponse } from "@server/routers/apiKeys"; -import { AxiosResponse } from "axios"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; - -export default function Page() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const { apiKeyId } = useParams(); - - const [loadingPage, setLoadingPage] = useState(true); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - const [loadingSavePermissions, setLoadingSavePermissions] = - useState(false); - - useEffect(() => { - async function load() { - setLoadingPage(true); - - const res = await api - .get< - AxiosResponse - >(`/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(`/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 && ( - - - - - Permissions - - - Determine what this API key can do - - - - - - - - - - - - )} - - ); -} diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx deleted file mode 100644 index c76b185..0000000 --- a/src/app/admin/api-keys/create/page.tsx +++ /dev/null @@ -1,402 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import HeaderTitle from "@app/components/SettingsSectionTitle"; -import { z } from "zod"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Input } from "@app/components/ui/input"; -import { InfoIcon } from "lucide-react"; -import { Button } from "@app/components/ui/button"; -import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { AxiosResponse } from "axios"; -import { useParams, useRouter } from "next/navigation"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator -} from "@app/components/ui/breadcrumb"; -import Link from "next/link"; -import { - CreateOrgApiKeyBody, - CreateOrgApiKeyResponse -} from "@server/routers/apiKeys"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import CopyToClipboard from "@app/components/CopyToClipboard"; -import moment from "moment"; -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; - -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; - -export default function Page() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const router = useRouter(); - - const [loadingPage, setLoadingPage] = useState(true); - const [createLoading, setCreateLoading] = useState(false); - const [apiKey, setApiKey] = useState(null); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - - const form = useForm({ - resolver: zodResolver(createFormSchema), - defaultValues: { - name: "" - } - }); - - const copiedForm = useForm({ - resolver: zodResolver(copiedFormSchema), - defaultValues: { - copied: false - } - }); - - async function onSubmit(data: CreateFormValues) { - setCreateLoading(true); - - let payload: CreateOrgApiKeyBody = { - name: data.name - }; - - const res = await api - .put>(`/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(`/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(`/admin/api-keys`); - } - - useEffect(() => { - const load = async () => { - setLoadingPage(false); - }; - - load(); - }, []); - - return ( - <> -
- - -
- - {!loadingPage && ( -
- - {!apiKey && ( - <> - - - - API Key Information - - - - -
- - ( - - - Name - - - - - - - )} - /> - - -
-
-
- - - - - Permissions - - - Determine what this API key can do - - - - - - - - )} - - {apiKey && ( - - - - Your API Key - - - - - - - Name - - - - - - - - Created - - - {moment( - apiKey.createdAt - ).format("lll")} - - - - - - - - Save Your API Key - - - You will only be able to see this - once. Make sure to copy it to a - secure place. - - - -

- Your API key is: -

- - - -
- - ( - -
- { - copiedForm.setValue( - "copied", - e as boolean - ); - }} - /> - -
- -
- )} - /> - - -
-
- )} -
- -
- {!apiKey && ( - - )} - {!apiKey && ( - - )} - - {apiKey && ( - - )} -
-
- )} - - ); -} diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx deleted file mode 100644 index b4a0080..0000000 --- a/src/app/admin/api-keys/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { ListRootApiKeysResponse } from "@server/routers/apiKeys"; -import ApiKeysTable, { ApiKeyRow } from "./ApiKeysTable"; - -type ApiKeyPageProps = {}; - -export const dynamic = "force-dynamic"; - -export default async function ApiKeysPage(props: ApiKeyPageProps) { - let apiKeys: ListRootApiKeysResponse["apiKeys"] = []; - try { - const res = await internal.get>( - `/api-keys`, - await authCookieHeader() - ); - apiKeys = res.data.data.apiKeys; - } catch (e) {} - - const rows: ApiKeyRow[] = apiKeys.map((key) => { - return { - name: key.name, - id: key.apiKeyId, - key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`, - createdAt: key.createdAt - }; - }); - - return ( - <> - - - - - ); -} diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index f7844c7..eba6bae 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -42,7 +42,6 @@ import { } from "@app/components/InfoSection"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { Badge } from "@app/components/ui/badge"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; const GeneralFormSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -68,7 +67,6 @@ export default function GeneralPage() { const { idpId } = useParams(); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); - const { isUnlocked } = useLicenseStatusContext(); const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`; @@ -232,7 +230,6 @@ export default function GeneralPage() { defaultChecked={form.getValues( "autoProvision" )} - disabled={!isUnlocked()} onCheckedChange={(checked) => { form.setValue( "autoProvision", @@ -240,14 +237,6 @@ export default function GeneralPage() { ); }} /> - {!isUnlocked() && ( - - Professional - - )} When enabled, users will be diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index d244e13..559c87e 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -4,17 +4,7 @@ import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; -import { ProfessionalContentOverlay } from "@app/components/ProfessionalContentOverlay"; -import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator -} from "@app/components/ui/breadcrumb"; interface SettingsLayoutProps { children: React.ReactNode; @@ -44,7 +34,6 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { { title: "Organization Policies", href: `/admin/idp/${params.idpId}/policies`, - showProfessional: true } ]; diff --git a/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx b/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx new file mode 100644 index 0000000..b967fc8 --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx @@ -0,0 +1,368 @@ +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { PolicyRow } from "./PolicyTable"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { cn } from "@app/lib/cn"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import type { Org } from "@server/db/schemas"; +import { AxiosResponse } from "axios"; +import { CreateIdpOrgPolicyResponse } from "@server/routers/idp"; +import { toast } from "@app/hooks/useToast"; + +type EditPolicyFormProps = { + idpId: string; + orgs: Org[]; + policies: PolicyRow[]; + policyToEdit: PolicyRow | null; + open: boolean; + setOpen: (open: boolean) => void; + afterCreate?: (policy: PolicyRow) => void; + afterEdit?: (policy: PolicyRow) => void; +}; + +const formSchema = z.object({ + orgId: z.string(), + roleMapping: z.string().optional(), + orgMapping: z.string().optional() +}); + +export default function EditPolicyForm({ + idpId, + orgs, + policies, + policyToEdit, + open, + setOpen, + afterCreate, + afterEdit +}: EditPolicyFormProps) { + const [loading, setLoading] = useState(false); + const [orgsPopoverOpen, setOrgsPopoverOpen] = useState(false); + + const api = createApiClient(useEnvContext()); + + const defaultValues = { + roleMapping: "", + orgMapping: "" + }; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues, + // @ts-ignore + values: policyToEdit + ? { + orgId: policyToEdit.orgId, + roleMapping: policyToEdit.roleMapping || "", + orgMapping: policyToEdit.orgMapping || "" + } + : defaultValues + }); + + async function onSubmit(values: z.infer) { + setLoading(true); + + if (policyToEdit) { + const res = await api + .post>( + `/idp/${idpId}/org/${values.orgId}`, + { + roleMapping: values.roleMapping, + orgMapping: values.orgMapping + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to create org policy", + description: formatAxiosError( + e, + "An error occurred while updating the org policy." + ) + }); + }); + + if (res && res.status === 200) { + toast({ + variant: "default", + title: "Org policy created", + description: "The org policy has been successfully updated." + }); + + setOpen(false); + + if (afterEdit) { + afterEdit({ + orgId: values.orgId, + roleMapping: values.roleMapping ?? null, + orgMapping: values.orgMapping ?? null + }); + } + } + } else { + const res = await api + .put>( + `/idp/${idpId}/org/${values.orgId}`, + { + roleMapping: values.roleMapping, + orgMapping: values.orgMapping + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to create role", + description: formatAxiosError( + e, + "An error occurred while creating the role." + ) + }); + }); + + if (res && res.status === 201) { + toast({ + variant: "default", + title: "Org policy created", + description: "The org policy has been successfully created." + }); + + setOpen(false); + + if (afterCreate) { + afterCreate({ + orgId: values.orgId, + roleMapping: values.roleMapping ?? null, + orgMapping: values.orgMapping ?? null + }); + } + } + } + + setLoading(false); + } + + return ( + { + setOpen(val); + setLoading(false); + setOrgsPopoverOpen(false); + form.reset(); + }} + > + + + + {policyToEdit ? "Edit" : "Create"} Organization Policy + + + Configure access for an organization + + + +
+ + ( + + Organization + {policyToEdit ? ( + + ) : ( + + + + + + + + + + + + No site found. + + + {orgs.map( + (org) => { + if ( + policies.find( + ( + p + ) => + p.orgId === + org.orgId + ) + ) { + return undefined; + } + return ( + { + form.setValue( + "orgId", + org.orgId + ); + setOrgsPopoverOpen( + false + ); + }} + > + + { + org.name + } + + ); + } + )} + + + + + + )} + + + )} + /> + ( + + + Role Mapping Path (Optional) + + + + + + JMESPath to extract role information + from the ID token. The result of + this expression must return the role + name(s) as defined in the + organization as a string/list of + strings. + + + + )} + /> + ( + + + Organization Mapping Path (Optional) + + + + + + JMESPath to extract organization + information from the ID token. This + expression must return thr org ID or + true for the user to be allowed to + access the organization. + + + + )} + /> + + +
+ + + + + + +
+
+ ); +} diff --git a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx index 222e98e..73ca2ff 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx @@ -1,8 +1,3 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - "use client"; import { ColumnDef } from "@tanstack/react-table"; @@ -11,7 +6,7 @@ import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; - onAdd: () => void; + onAdd?: () => void; } export function PolicyDataTable({ @@ -23,11 +18,11 @@ export function PolicyDataTable({ ); } diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx index df78c64..f1c8fb2 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx @@ -1,72 +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. - "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@app/components/ui/button"; -import { - ArrowUpDown, - Trash2, - MoreHorizontal, - Pencil, - ArrowRight -} from "lucide-react"; -import { PolicyDataTable } from "./PolicyDataTable"; -import { Badge } from "@app/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; -import Link from "next/link"; -import { InfoPopup } from "@app/components/ui/info-popup"; +import { Button } from "@app/components/ui/button"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { PolicyDataTable } from "./PolicyDataTable"; export interface PolicyRow { orgId: string; - roleMapping?: string; - orgMapping?: string; + roleMapping: string | null; + orgMapping: string | null; } -interface Props { +type PolicyTableProps = { policies: PolicyRow[]; - onDelete: (orgId: string) => void; onAdd: () => void; - onEdit: (policy: PolicyRow) => void; -} + onEdit: (row: PolicyRow) => void; + onDelete: (row: PolicyRow) => void; +}; -export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props) { +export default function PolicyTable({ + policies, + onAdd, + onEdit, + onDelete +}: PolicyTableProps) { const columns: ColumnDef[] = [ { - id: "dots", + id: "actions", cell: ({ row }) => { - const r = row.original; + const policyRow = row.original; return ( - - - - - - { - onDelete(r.orgId); - }} - > - Delete - - - + <> +
+ + + + + + onEdit(policyRow)} + > + Edit Policy + + onDelete(policyRow)} + > + + Delete Policy + + + + +
+ ); } }, { + id: "orgId", accessorKey: "orgId", header: ({ column }) => { return ( @@ -84,74 +90,24 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props }, { accessorKey: "roleMapping", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const mapping = row.original.roleMapping; - return mapping ? ( - 50 ? `${mapping.substring(0, 50)}...` : mapping} - info={mapping} - /> - ) : ( - "--" - ); - } + header: "Role Mapping" }, { accessorKey: "orgMapping", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const mapping = row.original.orgMapping; - return mapping ? ( - 50 ? `${mapping.substring(0, 50)}...` : mapping} - info={mapping} - /> - ) : ( - "--" - ); - } + header: "Organization Mapping" }, { - id: "actions", - cell: ({ row }) => { - const policy = row.original; - return ( -
- -
- ); - } + id: "edit", + cell: ({ row }) => ( +
+ +
+ ) } ]; diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index 9fb9b49..7114011 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -1,27 +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. - "use client"; -import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { Button } from "@app/components/ui/button"; -import { Input } from "@app/components/ui/input"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; import { Form, FormControl, @@ -31,33 +12,10 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; -import PolicyTable, { PolicyRow } from "./PolicyTable"; -import { AxiosResponse } from "axios"; -import { ListOrgsResponse } from "@server/routers/org"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; -import { Textarea } from "@app/components/ui/textarea"; -import { InfoPopup } from "@app/components/ui/info-popup"; -import { GetIdpResponse } from "@server/routers/idp"; +import { toast } from "@app/hooks/useToast"; +import { useRouter, useParams } from "next/navigation"; import { SettingsContainer, SettingsSection, @@ -65,64 +23,51 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm + SettingsSectionFooter } from "@app/components/Settings"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useState, useEffect } from "react"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon, ExternalLink } from "lucide-react"; +import Link from "next/link"; +import { AxiosResponse } from "axios"; +import { + GetIdpResponse, + ListIdpOrgPoliciesResponse +} from "@server/routers/idp"; +import PolicyTable, { PolicyRow } from "./PolicyTable"; +import EditPolicyForm from "./EditPolicyForm"; +import { ListOrgsResponse } from "@server/routers/org"; +import type { Org } from "@server/db/schemas"; -type Organization = { - orgId: string; - name: string; -}; - -const policyFormSchema = z.object({ - orgId: z.string().min(1, { message: "Organization is required" }), - roleMapping: z.string().optional(), - orgMapping: z.string().optional() -}); - -const defaultMappingsSchema = z.object({ +const DefaultMappingsFormSchema = z.object({ defaultRoleMapping: z.string().optional(), defaultOrgMapping: z.string().optional() }); -type PolicyFormValues = z.infer; -type DefaultMappingsValues = z.infer; +type DefaultMappingsFormValues = z.infer; export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const { idpId } = useParams(); - - const [pageLoading, setPageLoading] = useState(true); - const [addPolicyLoading, setAddPolicyLoading] = useState(false); - const [editPolicyLoading, setEditPolicyLoading] = useState(false); - const [deletePolicyLoading, setDeletePolicyLoading] = useState(false); + const [loading, setLoading] = useState(false); const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] = useState(false); const [policies, setPolicies] = useState([]); - const [organizations, setOrganizations] = useState([]); - const [showAddDialog, setShowAddDialog] = useState(false); - const [editingPolicy, setEditingPolicy] = useState(null); + const [editPolicyFormOpen, setEditPolicyFormOpen] = useState(false); + const [policyToEdit, setPolicyToEdit] = useState(null); + const [orgs, setOrgs] = useState([]); - const form = useForm({ - resolver: zodResolver(policyFormSchema), - defaultValues: { - orgId: "", - roleMapping: "", - orgMapping: "" - } + const defaultMappingsForm = useForm({ + resolver: zodResolver(DefaultMappingsFormSchema), + defaultValues: { defaultRoleMapping: "", defaultOrgMapping: "" } }); - const defaultMappingsForm = useForm({ - resolver: zodResolver(defaultMappingsSchema), - defaultValues: { - defaultRoleMapping: "", - defaultOrgMapping: "" - } - }); - - const loadIdp = async () => { + async function loadIdp() { try { const res = await api.get>( `/idp/${idpId}` @@ -141,11 +86,13 @@ export default function PoliciesPage() { variant: "destructive" }); } - }; + } - const loadPolicies = async () => { + async function loadIdpOrgPolicies() { try { - const res = await api.get(`/idp/${idpId}/org`); + const res = await api.get< + AxiosResponse + >(`/idp/${idpId}/org`); if (res.status === 200) { setPolicies(res.data.data.policies); } @@ -156,17 +103,13 @@ export default function PoliciesPage() { variant: "destructive" }); } - }; + } - const loadOrganizations = async () => { + async function loadOrgs() { try { - const res = await api.get>("/orgs"); + const res = await api.get>(`/orgs`); if (res.status === 200) { - const existingOrgIds = policies.map((p) => p.orgId); - const availableOrgs = res.data.data.orgs.filter( - (org) => !existingOrgIds.includes(org.orgId) - ); - setOrganizations(availableOrgs); + setOrgs(res.data.data.orgs); } } catch (e) { toast({ @@ -175,121 +118,19 @@ export default function PoliciesPage() { variant: "destructive" }); } - }; + } useEffect(() => { - async function load() { - setPageLoading(true); - await loadPolicies(); - await loadIdp(); - setPageLoading(false); - } + const load = async () => { + setLoading(true); + await Promise.all([loadIdp(), loadIdpOrgPolicies(), loadOrgs()]); + setLoading(false); + }; + load(); - }, [idpId]); + }, [idpId, api, router]); - const onAddPolicy = async (data: PolicyFormValues) => { - setAddPolicyLoading(true); - try { - const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - }); - if (res.status === 201) { - const newPolicy = { - orgId: data.orgId, - name: - organizations.find((org) => org.orgId === data.orgId) - ?.name || "", - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - }; - setPolicies([...policies, newPolicy]); - toast({ - title: "Success", - description: "Policy added successfully" - }); - setShowAddDialog(false); - form.reset(); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setAddPolicyLoading(false); - } - }; - - const onEditPolicy = async (data: PolicyFormValues) => { - if (!editingPolicy) return; - - setEditPolicyLoading(true); - try { - const res = await api.post( - `/idp/${idpId}/org/${editingPolicy.orgId}`, - { - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - } - ); - if (res.status === 200) { - setPolicies( - policies.map((policy) => - policy.orgId === editingPolicy.orgId - ? { - ...policy, - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - } - : policy - ) - ); - toast({ - title: "Success", - description: "Policy updated successfully" - }); - setShowAddDialog(false); - setEditingPolicy(null); - form.reset(); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setEditPolicyLoading(false); - } - }; - - const onDeletePolicy = async (orgId: string) => { - setDeletePolicyLoading(true); - try { - const res = await api.delete(`/idp/${idpId}/org/${orgId}`); - if (res.status === 200) { - setPolicies( - policies.filter((policy) => policy.orgId !== orgId) - ); - toast({ - title: "Success", - description: "Policy deleted successfully" - }); - } - } catch (e) { - toast({ - title: "Error", - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setDeletePolicyLoading(false); - } - }; - - const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { + async function onDefaultMappingsSubmit(data: DefaultMappingsFormValues) { setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { @@ -311,26 +152,60 @@ export default function PoliciesPage() { } finally { setUpdateDefaultMappingsLoading(false); } - }; + } - if (pageLoading) { - return null; + // Button clicks + + function onAdd() { + setPolicyToEdit(null); + setEditPolicyFormOpen(true); + } + function onEdit(row: PolicyRow) { + setPolicyToEdit(row); + setEditPolicyFormOpen(true); + } + function onDelete(row: PolicyRow) { + api.delete(`/idp/${idpId}/org/${row.orgId}`) + .then((res) => { + if (res.status === 200) { + toast({ + title: "Success", + description: "Org policy deleted successfully" + }); + const p2 = policies.filter((p) => p.orgId !== row.orgId); + setPolicies(p2); + } + }) + .catch((e) => { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + }); + } + + function afterCreate(row: PolicyRow) { + setPolicies([...policies, row]); + } + + function afterEdit(row: PolicyRow) { + const p2 = policies.map((p) => (p.orgId === row.orgId ? row : p)); + setPolicies(p2); } return ( <> - + About Organization Policies - Organization policies are used to control access to - organizations based on the user's ID token. You can - specify JMESPath expressions to extract role and - organization information from the ID token. For more - information, see{" "} + Organization policies are used to configure access + control for a specific organization based on the user's + ID token. For more information, see{" "} - The default mappings are used when when there is not - an organization policy defined for an organization. - You can specify the default role and organization + The default mappings are used when there is no + organization policy defined for an organization. You + can specify the default role and organization mappings to fall back to here. @@ -359,10 +234,10 @@ export default function PoliciesPage() {
)} /> - { - loadOrganizations(); - form.reset({ - orgId: "", - roleMapping: "", - orgMapping: "" - }); - setEditingPolicy(null); - setShowAddDialog(true); - }} - onEdit={(policy) => { - setEditingPolicy(policy); - form.reset({ - orgId: policy.orgId, - roleMapping: policy.roleMapping || "", - orgMapping: policy.orgMapping || "" - }); - setShowAddDialog(true); - }} + onAdd={onAdd} + onEdit={onEdit} + onDelete={onDelete} + /> + + - - { - setShowAddDialog(val); - setEditingPolicy(null); - form.reset(); - }} - > - - - - {editingPolicy - ? "Edit Organization Policy" - : "Add Organization Policy"} - - - Configure access for an organization - - - - - - ( - - Organization - {editingPolicy ? ( - - ) : ( - - - - - - - - - - - - No org - found. - - - {organizations.map( - ( - org - ) => ( - { - form.setValue( - "orgId", - org.orgId - ); - }} - > - - { - org.name - } - - ) - )} - - - - - - )} - - - )} - /> - - ( - - - Role Mapping Path (Optional) - - - - - - JMESPath to extract role - information from the ID token. - The result of this expression - must return the role name as - defined in the organization as a - string. - - - - )} - /> - - ( - - - Organization Mapping Path - (Optional) - - - - - - JMESPath to extract organization - information from the ID token. - This expression must return the - org ID or true for the user to - be allowed to access the - organization. - - - - )} - /> - - - - - - - - - - - ); } diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 034cc69..58e6667 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -36,7 +36,6 @@ import { InfoIcon, ExternalLink } from "lucide-react"; import { StrategySelect } from "@app/components/StrategySelect"; import { SwitchInput } from "@app/components/SwitchInput"; import { Badge } from "@app/components/ui/badge"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; const createIdpFormSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -75,7 +74,6 @@ export default function Page() { const api = createApiClient({ env }); const router = useRouter(); const [createLoading, setCreateLoading] = useState(false); - const { isUnlocked } = useLicenseStatusContext(); const form = useForm({ resolver: zodResolver(createIdpFormSchema), @@ -192,7 +190,6 @@ export default function Page() { defaultChecked={form.getValues( "autoProvision" )} - disabled={!isUnlocked()} onCheckedChange={(checked) => { form.setValue( "autoProvision", @@ -200,14 +197,6 @@ export default function Page() { ); }} /> - {!isUnlocked() && ( - - Professional - - )}
When enabled, users will be diff --git a/src/app/admin/license/LicenseKeysDataTable.tsx b/src/app/admin/license/LicenseKeysDataTable.tsx deleted file mode 100644 index 98ed814..0000000 --- a/src/app/admin/license/LicenseKeysDataTable.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@app/components/ui/data-table"; -import { Button } from "@app/components/ui/button"; -import { Badge } from "@app/components/ui/badge"; -import { LicenseKeyCache } from "@server/license/license"; -import { ArrowUpDown } from "lucide-react"; -import moment from "moment"; -import CopyToClipboard from "@app/components/CopyToClipboard"; - -type LicenseKeysDataTableProps = { - licenseKeys: LicenseKeyCache[]; - onDelete: (key: LicenseKeyCache) => void; - onCreate: () => void; -}; - -function obfuscateLicenseKey(key: string): string { - if (key.length <= 8) return key; - const firstPart = key.substring(0, 4); - const lastPart = key.substring(key.length - 4); - return `${firstPart}••••••••••••••••••••${lastPart}`; -} - -export function LicenseKeysDataTable({ - licenseKeys, - onDelete, - onCreate -}: LicenseKeysDataTableProps) { - const columns: ColumnDef[] = [ - { - accessorKey: "licenseKey", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const licenseKey = row.original.licenseKey; - return ( - - ); - } - }, - { - accessorKey: "valid", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return row.original.valid ? "Yes" : "No"; - } - }, - { - accessorKey: "type", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const type = row.original.type; - const label = - type === "SITES" ? "Additional Sites" : "Host License"; - const variant = type === "SITES" ? "secondary" : "default"; - return row.original.valid ? ( - {label} - ) : null; - } - }, - { - accessorKey: "numSites", - header: ({ column }) => { - return ( - - ); - } - }, - { - id: "delete", - cell: ({ row }) => ( -
- -
- ) - } - ]; - - return ( - - ); -} diff --git a/src/app/admin/license/components/SitePriceCalculator.tsx b/src/app/admin/license/components/SitePriceCalculator.tsx deleted file mode 100644 index cf771b5..0000000 --- a/src/app/admin/license/components/SitePriceCalculator.tsx +++ /dev/null @@ -1,166 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { useState } from "react"; -import { Button } from "@app/components/ui/button"; -import { MinusCircle, PlusCircle } from "lucide-react"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; - -type SitePriceCalculatorProps = { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - mode: "license" | "additional-sites"; -}; - -export function SitePriceCalculator({ - isOpen, - onOpenChange, - mode -}: SitePriceCalculatorProps) { - const [siteCount, setSiteCount] = useState(3); - const pricePerSite = 5; - const licenseFlatRate = 125; - - const incrementSites = () => { - setSiteCount((prev) => prev + 1); - }; - - const decrementSites = () => { - setSiteCount((prev) => (prev > 1 ? prev - 1 : 1)); - }; - - function continueToPayment() { - if (mode === "license") { - // open in new tab - window.open( - `https://payment.fossorial.io/buy/dab98d3d-9976-49b1-9e55-1580059d833f?quantity=${siteCount}`, - "_blank" - ); - } else { - window.open( - `https://payment.fossorial.io/buy/2b881c36-ea5d-4c11-8652-9be6810a054f?quantity=${siteCount}`, - "_blank" - ); - } - } - - const totalCost = - mode === "license" - ? licenseFlatRate + siteCount * pricePerSite - : siteCount * pricePerSite; - - return ( - - - - - {mode === "license" - ? "Purchase License" - : "Purchase Additional Sites"} - - - Choose how many sites you want to{" "} - {mode === "license" - ? "purchase a license for. You can always add more sites later." - : "add to your existing license."} - - - -
-
-
- Number of Sites -
-
- - - {siteCount} - - -
-
- -
- {mode === "license" && ( -
- - License fee: - - - ${licenseFlatRate.toFixed(2)} - -
- )} -
- - Price per site: - - - ${pricePerSite.toFixed(2)} - -
-
- - Number of sites: - - {siteCount} -
-
- Total: - ${totalCost.toFixed(2)} / mo -
- -

- For the most up-to-date pricing and discounts, - please visit the{" "} - - pricing page - - . -

-
-
-
- - - - - - -
-
- ); -} diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx deleted file mode 100644 index a967889..0000000 --- a/src/app/admin/license/page.tsx +++ /dev/null @@ -1,528 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { useState, useEffect } from "react"; -import { LicenseKeyCache } from "@server/license/license"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { formatAxiosError } from "@app/lib/api"; -import { LicenseKeysDataTable } from "./LicenseKeysDataTable"; -import { AxiosResponse } from "axios"; -import { Button } from "@app/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { useRouter } from "next/navigation"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { - SettingsContainer, - SettingsSectionTitle as SSTitle, - SettingsSection, - SettingsSectionDescription, - SettingsSectionGrid, - SettingsSectionHeader, - SettingsSectionFooter -} from "@app/components/Settings"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { Badge } from "@app/components/ui/badge"; -import { Check, ShieldCheck, ShieldOff } from "lucide-react"; -import CopyTextBox from "@app/components/CopyTextBox"; -import { Progress } from "@app/components/ui/progress"; -import { MinusCircle, PlusCircle } from "lucide-react"; -import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import { SitePriceCalculator } from "./components/SitePriceCalculator"; -import Link from "next/link"; -import { Checkbox } from "@app/components/ui/checkbox"; - -const formSchema = z.object({ - licenseKey: z - .string() - .nonempty({ message: "License key is required" }) - .max(255), - agreeToTerms: z.boolean().refine((val) => val === true, { - message: "You must agree to the license terms" - }) -}); - -function obfuscateLicenseKey(key: string): string { - if (key.length <= 8) return key; - const firstPart = key.substring(0, 4); - const lastPart = key.substring(key.length - 4); - return `${firstPart}••••••••••••••••••••${lastPart}`; -} - -export default function LicensePage() { - const api = createApiClient(useEnvContext()); - const [rows, setRows] = useState([]); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedLicenseKey, setSelectedLicenseKey] = - useState(null); - const router = useRouter(); - const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext(); - const [hostLicense, setHostLicense] = useState(null); - const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false); - const [purchaseMode, setPurchaseMode] = useState< - "license" | "additional-sites" - >("license"); - - // Separate loading states for different actions - const [isInitialLoading, setIsInitialLoading] = useState(true); - const [isActivatingLicense, setIsActivatingLicense] = useState(false); - const [isDeletingLicense, setIsDeletingLicense] = useState(false); - const [isRecheckingLicense, setIsRecheckingLicense] = useState(false); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - licenseKey: "", - agreeToTerms: false - } - }); - - useEffect(() => { - async function load() { - setIsInitialLoading(true); - await loadLicenseKeys(); - setIsInitialLoading(false); - } - load(); - }, []); - - async function loadLicenseKeys() { - try { - const response = - await api.get>( - "/license/keys" - ); - const keys = response.data.data; - setRows(keys); - const hostKey = keys.find((key) => key.type === "HOST"); - if (hostKey) { - setHostLicense(hostKey.licenseKey); - } else { - setHostLicense(null); - } - } catch (e) { - toast({ - title: "Failed to load license keys", - description: formatAxiosError( - e, - "An error occurred loading license keys" - ) - }); - } - } - - async function deleteLicenseKey(key: string) { - try { - setIsDeletingLicense(true); - const encodedKey = encodeURIComponent(key); - const res = await api.delete(`/license/${encodedKey}`); - if (res.data.data) { - updateLicenseStatus(res.data.data); - } - await loadLicenseKeys(); - toast({ - title: "License key deleted", - description: "The license key has been deleted" - }); - setIsDeleteModalOpen(false); - } catch (e) { - toast({ - title: "Failed to delete license key", - description: formatAxiosError( - e, - "An error occurred deleting license key" - ) - }); - } finally { - setIsDeletingLicense(false); - } - } - - async function recheck() { - try { - setIsRecheckingLicense(true); - const res = await api.post(`/license/recheck`); - if (res.data.data) { - updateLicenseStatus(res.data.data); - } - await loadLicenseKeys(); - toast({ - title: "License keys rechecked", - description: "All license keys have been rechecked" - }); - } catch (e) { - toast({ - title: "Failed to recheck license keys", - description: formatAxiosError( - e, - "An error occurred rechecking license keys" - ) - }); - } finally { - setIsRecheckingLicense(false); - } - } - - async function onSubmit(values: z.infer) { - try { - setIsActivatingLicense(true); - const res = await api.post("/license/activate", { - licenseKey: values.licenseKey - }); - if (res.data.data) { - updateLicenseStatus(res.data.data); - } - - toast({ - title: "License key activated", - description: "The license key has been successfully activated." - }); - - setIsCreateModalOpen(false); - form.reset(); - await loadLicenseKeys(); - } catch (e) { - toast({ - variant: "destructive", - title: "Failed to activate license key", - description: formatAxiosError( - e, - "An error occurred while activating the license key." - ) - }); - } finally { - setIsActivatingLicense(false); - } - } - - if (isInitialLoading) { - return null; - } - - return ( - <> - { - setIsPurchaseModalOpen(val); - }} - mode={purchaseMode} - /> - - { - setIsCreateModalOpen(val); - form.reset(); - }} - > - - - Activate License Key - - Enter a license key to activate it. - - - -
- - ( - - License Key - - - - - - )} - /> - ( - - - - -
- - By checking this box, you - confirm that you have read - and agree to the license - terms corresponding to the - tier associated with your - license key. -
- - View Fossorial - Commercial License & - Subscription Terms - -
- -
-
- )} - /> - - -
- - - - - - -
-
- - {selectedLicenseKey && ( - { - setIsDeleteModalOpen(val); - setSelectedLicenseKey(null); - }} - dialog={ -
-

- Are you sure you want to delete the license key{" "} - - {obfuscateLicenseKey( - selectedLicenseKey.licenseKey - )} - - ? -

-

- - This will remove the license key and all - associated permissions granted by it. - -

-

- To confirm, please type the license key below. -

-
- } - buttonText="Confirm Delete License Key" - onConfirm={async () => - deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted) - } - string={selectedLicenseKey.licenseKey} - title="Delete License Key" - /> - )} - - - - - - - - Host License - - Manage the main license key for the host. - - -
-
- {licenseStatus?.isLicenseValid ? ( -
-
- - {licenseStatus?.tier === - "PROFESSIONAL" - ? "Professional License" - : licenseStatus?.tier === - "ENTERPRISE" - ? "Enterprise License" - : "Licensed"} -
-
- ) : ( -
-
- Not Licensed -
-
- )} -
- {licenseStatus?.hostId && ( -
-
- Host ID -
- -
- )} - {hostLicense && ( -
-
- License Key -
- -
- )} -
- - - -
- - - Sites Usage - - View the number of sites using this license. - - -
-
-
- {licenseStatus?.usedSites || 0}{" "} - {licenseStatus?.usedSites === 1 - ? "site" - : "sites"}{" "} - in system -
-
- {!licenseStatus?.isHostLicensed && ( -

- There is no limit on the number of sites - using an unlicensed host. -

- )} - {licenseStatus?.maxSites && ( -
-
- - {licenseStatus.usedSites || 0} of{" "} - {licenseStatus.maxSites} sites used - - - {Math.round( - ((licenseStatus.usedSites || - 0) / - licenseStatus.maxSites) * - 100 - )} - % - -
- -
- )} -
- - {!licenseStatus?.isHostLicensed ? ( - <> - - - ) : ( - <> - - - )} - -
-
- { - setSelectedLicenseKey(key); - setIsDeleteModalOpen(true); - }} - onCreate={() => setIsCreateModalOpen(true)} - /> -
- - ); -} diff --git a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx index 87a7683..c946869 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx @@ -15,7 +15,6 @@ import { } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; type ValidateOidcTokenParams = { orgId: string; @@ -34,8 +33,6 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { licenseStatus, isLicenseViolation } = useLicenseStatusContext(); - useEffect(() => { async function validate() { setLoading(true); @@ -46,10 +43,6 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { stateCookie: props.stateCookie }); - if (isLicenseViolation()) { - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - try { const res = await api.post< AxiosResponse diff --git a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx index c7eca2c..f7d726e 100644 --- a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx @@ -43,7 +43,6 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import Link from "next/link"; -import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; const pinSchema = z.object({ pin: z @@ -110,8 +109,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const api = createApiClient({ env }); - const { supporterStatus } = useSupporterStatusContext(); - function getDefaultSelectedMethod() { if (props.methods.sso) { return "sso"; @@ -634,15 +631,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { - {supporterStatus?.visible && ( -
- - Server is running without a supporter key. -
- Consider supporting the project! -
-
- )} ) : ( diff --git a/src/app/components/LicenseViolation.tsx b/src/app/components/LicenseViolation.tsx deleted file mode 100644 index 75d544d..0000000 --- a/src/app/components/LicenseViolation.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { Button } from "@app/components/ui/button"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useState } from "react"; - -export default function LicenseViolation() { - const { licenseStatus } = useLicenseStatusContext(); - const [isDismissed, setIsDismissed] = useState(false); - - if (!licenseStatus || isDismissed) return null; - - // Show invalid license banner - if (licenseStatus.isHostLicensed && !licenseStatus.isLicenseValid) { - return ( -
-
-

- Invalid or expired license keys detected. Follow license - terms to continue using all features. -

- -
-
- ); - } - - // Show usage violation banner - if ( - licenseStatus.maxSites && - licenseStatus.usedSites && - licenseStatus.usedSites > licenseStatus.maxSites - ) { - return ( -
-
-

- License Violation: This server is using{" "} - {licenseStatus.usedSites} sites which exceeds its - licensed limit of {licenseStatus.maxSites} sites. Follow - license terms to continue using all features. -

- -
-
- ); - } - - return null; -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e0089bc..d99c026 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,13 +5,6 @@ import { Toaster } from "@/components/ui/toaster"; import { ThemeProvider } from "@app/providers/ThemeProvider"; import EnvProvider from "@app/providers/EnvProvider"; import { pullEnv } from "@app/lib/pullEnv"; -import SupportStatusProvider from "@app/providers/SupporterStatusProvider"; -import { priv } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey"; -import LicenseStatusProvider from "@app/providers/LicenseStatusProvider"; -import { GetLicenseStatusResponse } from "@server/routers/license"; -import LicenseViolation from "./components/LicenseViolation"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -30,22 +23,6 @@ export default async function RootLayout({ }>) { const env = pullEnv(); - let supporterData = { - visible: true - } as any; - - const res = await priv.get>( - "supporter-key/visible" - ); - supporterData.visible = res.data.data.visible; - supporterData.tier = res.data.data.tier; - - const licenseStatusRes = - await priv.get>( - "/license/status" - ); - const licenseStatus = licenseStatusRes.data.data; - return ( @@ -56,19 +33,12 @@ export default async function RootLayout({ disableTransitionOnChange > - - - {/* Main content */} -
-
- - {children} -
-
-
-
+ {/* Main content */} +
+
+ {children} +
+
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index b05bf30..821f12c 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -65,12 +65,14 @@ export const orgNavItems: SidebarNavItem[] = [ href: "/{orgId}/settings/share-links", icon: }, + /* + TODO: { title: "API Keys", href: "/{orgId}/settings/api-keys", icon: , - showProfessional: true }, + */ { title: "Settings", href: "/{orgId}/settings/general", @@ -84,20 +86,17 @@ export const adminNavItems: SidebarNavItem[] = [ href: "/admin/users", icon: }, + /* + TODO: { title: "API Keys", href: "/admin/api-keys", icon: , - showProfessional: true }, + */ { title: "Identity Providers", href: "/admin/idp", icon: - }, - { - title: "License", - href: "/admin/license", - icon: } ]; diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx index eb590eb..5d17fe3 100644 --- a/src/components/HorizontalTabs.tsx +++ b/src/components/HorizontalTabs.tsx @@ -4,15 +4,11 @@ import React from "react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; import { cn } from "@app/lib/cn"; -import { buttonVariants } from "@/components/ui/button"; -import { Badge } from "@app/components/ui/badge"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; export type HorizontalTabs = Array<{ title: string; href: string; icon?: React.ReactNode; - showProfessional?: boolean; }>; interface HorizontalTabsProps { @@ -28,7 +24,6 @@ export function HorizontalTabs({ }: HorizontalTabsProps) { const pathname = usePathname(); const params = useParams(); - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); function hydrateHref(href: string) { return href @@ -49,46 +44,34 @@ export function HorizontalTabs({ const isActive = pathname.startsWith(hydratedHref) && !pathname.includes("create"); - const isProfessional = - item.showProfessional && !isUnlocked(); - const isDisabled = - disabled || (isProfessional && !isUnlocked()); return ( { - if (isDisabled) { + if (disabled) { e.preventDefault(); } }} - tabIndex={isDisabled ? -1 : undefined} - aria-disabled={isDisabled} + tabIndex={disabled ? -1 : undefined} + aria-disabled={disabled} >
{item.icon && item.icon} {item.title} - {isProfessional && ( - - Professional - - )}
); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 12cb09d..ad008d7 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -5,7 +5,6 @@ import { SidebarNav } from "@app/components/SidebarNav"; import { OrgSelector } from "@app/components/OrgSelector"; import { cn } from "@app/lib/cn"; import { ListUserOrgsResponse } from "@server/routers/org"; -import SupporterStatus from "@app/components/SupporterStatus"; import { Button } from "@app/components/ui/button"; import { ExternalLink, Menu, X, Server } from "lucide-react"; import Image from "next/image"; @@ -118,7 +117,6 @@ export function Layout({ )}
-
-
diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx deleted file mode 100644 index a6f9add..0000000 --- a/src/components/PermissionsSelectBox.tsx +++ /dev/null @@ -1,235 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { CheckboxWithLabel } from "@app/components/ui/checkbox"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; - -type PermissionsSelectBoxProps = { - root?: boolean; - selectedPermissions: Record; - onChange: (updated: Record) => void; -}; - -function getActionsCategories(root: boolean) { - const actionsByCategory: Record> = { - Organization: { - "Get Organization": "getOrg", - "Update Organization": "updateOrg", - "Get Organization User": "getOrgUser", - "List Organization Domains": "listOrgDomains", - "Check Org ID": "checkOrgId", - }, - - Site: { - "Create Site": "createSite", - "Delete Site": "deleteSite", - "Get Site": "getSite", - "List Sites": "listSites", - "Update Site": "updateSite", - "List Allowed Site Roles": "listSiteRoles" - }, - - Resource: { - "Create Resource": "createResource", - "Delete Resource": "deleteResource", - "Get Resource": "getResource", - "List Resources": "listResources", - "Update Resource": "updateResource", - "List Resource Users": "listResourceUsers", - "Set Resource Users": "setResourceUsers", - "Set Allowed Resource Roles": "setResourceRoles", - "List Allowed Resource Roles": "listResourceRoles", - "Set Resource Password": "setResourcePassword", - "Set Resource Pincode": "setResourcePincode", - "Set Resource Email Whitelist": "setResourceWhitelist", - "Get Resource Email Whitelist": "getResourceWhitelist" - }, - - Target: { - "Create Target": "createTarget", - "Delete Target": "deleteTarget", - "Get Target": "getTarget", - "List Targets": "listTargets", - "Update Target": "updateTarget" - }, - - Role: { - "Create Role": "createRole", - "Delete Role": "deleteRole", - "Get Role": "getRole", - "List Roles": "listRoles", - "Update Role": "updateRole", - "List Allowed Role Resources": "listRoleResources" - }, - - User: { - "Invite User": "inviteUser", - "Remove User": "removeUser", - "List Users": "listUsers", - "Add User Role": "addUserRole" - }, - - "Access Token": { - "Generate Access Token": "generateAccessToken", - "Delete Access Token": "deleteAcessToken", - "List Access Tokens": "listAccessTokens" - }, - - "Resource Rule": { - "Create Resource Rule": "createResourceRule", - "Delete Resource Rule": "deleteResourceRule", - "List Resource Rules": "listResourceRules", - "Update Resource Rule": "updateResourceRule" - } - }; - - if (root) { - actionsByCategory["Organization"] = { - "List Organizations": "listOrgs", - "Check ID": "checkOrgId", - "Create Organization": "createOrg", - "Delete Organization": "deleteOrg", - "List API Keys": "listApiKeys", - "List API Key Actions": "listApiKeyActions", - "Set API Key Allowed Actions": "setApiKeyActions", - "Create API Key": "createApiKey", - "Delete API Key": "deleteApiKey", - ...actionsByCategory["Organization"] - }; - - actionsByCategory["Identity Provider (IDP)"] = { - "Create IDP": "createIdp", - "Update IDP": "updateIdp", - "Delete IDP": "deleteIdp", - "List IDP": "listIdps", - "Get IDP": "getIdp", - "Create IDP Org Policy": "createIdpOrg", - "Delete IDP Org Policy": "deleteIdpOrg", - "List IDP Orgs": "listIdpOrgs", - "Update IDP Org": "updateIdpOrg" - }; - } - - return actionsByCategory; -} - -export default function PermissionsSelectBox({ - root, - selectedPermissions, - onChange -}: PermissionsSelectBoxProps) { - const actionsByCategory = getActionsCategories(root ?? false); - - const togglePermission = (key: string, checked: boolean) => { - onChange({ - ...selectedPermissions, - [key]: checked - }); - }; - - const areAllCheckedInCategory = (actions: Record) => { - return Object.values(actions).every( - (action) => selectedPermissions[action] - ); - }; - - const toggleAllInCategory = ( - actions: Record, - value: boolean - ) => { - const updated = { ...selectedPermissions }; - Object.values(actions).forEach((action) => { - updated[action] = value; - }); - onChange(updated); - }; - - const allActions = Object.values(actionsByCategory).flatMap(Object.values); - const allPermissionsChecked = allActions.every( - (action) => selectedPermissions[action] - ); - - const toggleAllPermissions = (checked: boolean) => { - const updated: Record = {}; - allActions.forEach((action) => { - updated[action] = checked; - }); - onChange(updated); - }; - - return ( - <> -
- - toggleAllPermissions(checked as boolean) - } - /> -
- - {Object.entries(actionsByCategory).map( - ([category, actions]) => { - const allChecked = areAllCheckedInCategory(actions); - return ( - - {category} - -
- - toggleAllInCategory( - actions, - checked as boolean - ) - } - /> - {Object.entries(actions).map( - ([label, value]) => ( - - togglePermission( - value, - checked as boolean - ) - } - /> - ) - )} -
-
-
- ); - } - )} -
- - ); -} diff --git a/src/components/ProfessionalContentOverlay.tsx b/src/components/ProfessionalContentOverlay.tsx deleted file mode 100644 index cd484a2..0000000 --- a/src/components/ProfessionalContentOverlay.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import { cn } from "@app/lib/cn"; - -type ProfessionalContentOverlayProps = { - children: React.ReactNode; - isProfessional?: boolean; -}; - -export function ProfessionalContentOverlay({ - children, - isProfessional = false -}: ProfessionalContentOverlayProps) { - return ( -
- {isProfessional && ( -
-
-

- Professional Edition Required -

-

- This feature is only available in the Professional - Edition. -

-
-
- )} - {children} -
- ); -} diff --git a/src/components/ProfileIcon.tsx b/src/components/ProfileIcon.tsx index 55b939f..aa43578 100644 --- a/src/components/ProfileIcon.tsx +++ b/src/components/ProfileIcon.tsx @@ -14,14 +14,13 @@ import { import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; -import { Laptop, LogOut, Moon, Sun } from "lucide-react"; +import { Laptop, Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useUserContext } from "@app/hooks/useUserContext"; import Disable2FaForm from "./Disable2FaForm"; import Enable2FaForm from "./Enable2FaForm"; -import SupporterStatus from "./SupporterStatus"; import { UserType } from "@server/types/UserTypes"; export default function ProfileIcon() { diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index d6de961..1df7f71 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -6,8 +6,6 @@ import { useParams, usePathname } from "next/navigation"; import { cn } from "@app/lib/cn"; import { ChevronDown, ChevronRight } from "lucide-react"; import { useUserContext } from "@app/hooks/useUserContext"; -import { Badge } from "@app/components/ui/badge"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; export interface SidebarNavItem { href: string; @@ -15,7 +13,6 @@ export interface SidebarNavItem { icon?: React.ReactNode; children?: SidebarNavItem[]; autoExpand?: boolean; - showProfessional?: boolean; } export interface SidebarNavProps extends React.HTMLAttributes { @@ -61,7 +58,6 @@ export function SidebarNav({ findAutoExpandedAndActivePath(items); return autoExpanded; }); - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const { user } = useUserContext(); @@ -92,8 +88,6 @@ export function SidebarNav({ const hasChildren = item.children && item.children.length > 0; const isExpanded = expandedItems.has(hydratedHref); const indent = level * 28; // Base indent for each level - const isProfessional = item.showProfessional && !isUnlocked(); - const isDisabled = disabled || isProfessional; return (
@@ -108,28 +102,28 @@ export function SidebarNav({ )} > { - if (isDisabled) { + if (disabled) { e.preventDefault(); } else if (onItemClick) { onItemClick(); } }} - tabIndex={isDisabled ? -1 : undefined} - aria-disabled={isDisabled} + tabIndex={disabled ? -1 : undefined} + aria-disabled={disabled} >
{item.icon && ( @@ -139,20 +133,12 @@ export function SidebarNav({ )} {item.title}
- {isProfessional && ( - - Professional - - )} {hasChildren && ( - - - - - - - Limited Supporter - - -

$25

-
    -
  • - - - For 5 or less users - -
  • -
  • - - - Lifetime purchase - -
  • -
  • - - - Supporter status - -
  • -
-
- - {supporterStatus?.tier !== - "Limited Supporter" ? ( - - - - ) : ( - - )} - -
-
-
- -
- - -
- - - - - - - - - - { - setKeyOpen(val); - }} - > - - - Enter Supporter Key - - Meet your very own pet Pangolin! - - - -
- - ( - - - GitHub Username - - - - - - - )} - /> - ( - - Supporter Key - - - - - - )} - /> - - -
- - - - - - -
-
- - {supporterStatus?.visible ? ( - - ) : null} - - ); -} diff --git a/src/contexts/apiKeyContext.ts b/src/contexts/apiKeyContext.ts deleted file mode 100644 index dd6c9b8..0000000 --- a/src/contexts/apiKeyContext.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { GetApiKeyResponse } from "@server/routers/apiKeys"; -import { createContext } from "react"; - -interface ApiKeyContextType { - apiKey: GetApiKeyResponse; - updateApiKey: (updatedApiKey: Partial) => void; -} - -const ApiKeyContext = createContext(undefined); - -export default ApiKeyContext; diff --git a/src/contexts/licenseStatusContext.ts b/src/contexts/licenseStatusContext.ts deleted file mode 100644 index eca6357..0000000 --- a/src/contexts/licenseStatusContext.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { LicenseStatus } from "@server/license/license"; -import { createContext } from "react"; - -type LicenseStatusContextType = { - licenseStatus: LicenseStatus | null; - updateLicenseStatus: (updatedSite: LicenseStatus) => void; - isLicenseViolation: () => boolean; - isUnlocked: () => boolean; -}; - -const LicenseStatusContext = createContext< - LicenseStatusContextType | undefined ->(undefined); - -export default LicenseStatusContext; diff --git a/src/contexts/supporterStatusContext.ts b/src/contexts/supporterStatusContext.ts deleted file mode 100644 index c5c8454..0000000 --- a/src/contexts/supporterStatusContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createContext } from "react"; - -export type SupporterStatus = { - visible: boolean; - tier?: string; -}; - -type SupporterStatusContextType = { - supporterStatus: SupporterStatus | null; - updateSupporterStatus: (updatedSite: Partial) => void; -}; - -const SupporterStatusContext = createContext< - SupporterStatusContextType | undefined ->(undefined); - -export default SupporterStatusContext; diff --git a/src/hooks/useApikeyContext.ts b/src/hooks/useApikeyContext.ts deleted file mode 100644 index 3ebcbdd..0000000 --- a/src/hooks/useApikeyContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import ApiKeyContext from "@app/contexts/apiKeyContext"; -import { useContext } from "react"; - -export function useApiKeyContext() { - const context = useContext(ApiKeyContext); - if (context === undefined) { - throw new Error( - "useApiKeyContext must be used within a ApiKeyProvider" - ); - } - return context; -} diff --git a/src/hooks/useLicenseStatusContext.ts b/src/hooks/useLicenseStatusContext.ts deleted file mode 100644 index b1da343..0000000 --- a/src/hooks/useLicenseStatusContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import LicenseStatusContext from "@app/contexts/licenseStatusContext"; -import { useContext } from "react"; - -export function useLicenseStatusContext() { - const context = useContext(LicenseStatusContext); - if (context === undefined) { - throw new Error( - "useLicenseStatusContext must be used within an LicenseStatusProvider" - ); - } - return context; -} diff --git a/src/hooks/useSupporterStatusContext.ts b/src/hooks/useSupporterStatusContext.ts deleted file mode 100644 index 359b401..0000000 --- a/src/hooks/useSupporterStatusContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import SupporterStatusContext from "@app/contexts/supporterStatusContext"; -import { useContext } from "react"; - -export function useSupporterStatusContext() { - const context = useContext(SupporterStatusContext); - if (context === undefined) { - throw new Error( - "useSupporterStatusContext must be used within an SupporterStatusProvider" - ); - } - return context; -} diff --git a/src/providers/ApiKeyProvider.tsx b/src/providers/ApiKeyProvider.tsx deleted file mode 100644 index 13061da..0000000 --- a/src/providers/ApiKeyProvider.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import ApiKeyContext from "@app/contexts/apiKeyContext"; -import { GetApiKeyResponse } from "@server/routers/apiKeys"; -import { useState } from "react"; - -interface ApiKeyProviderProps { - children: React.ReactNode; - apiKey: GetApiKeyResponse; -} - -export function ApiKeyProvider({ children, apiKey: ak }: ApiKeyProviderProps) { - const [apiKey, setApiKey] = useState(ak); - - const updateApiKey = (updatedApiKey: Partial) => { - if (!apiKey) { - throw new Error("No API key to update"); - } - setApiKey((prev) => { - if (!prev) { - return prev; - } - return { - ...prev, - ...updatedApiKey - }; - }); - }; - - return ( - - {children} - - ); -} - -export default ApiKeyProvider; diff --git a/src/providers/LicenseStatusProvider.tsx b/src/providers/LicenseStatusProvider.tsx deleted file mode 100644 index c3fe968..0000000 --- a/src/providers/LicenseStatusProvider.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -"use client"; - -import LicenseStatusContext from "@app/contexts/licenseStatusContext"; -import { LicenseStatus } from "@server/license/license"; -import { useState } from "react"; - -interface ProviderProps { - children: React.ReactNode; - licenseStatus: LicenseStatus | null; -} - -export function LicenseStatusProvider({ - children, - licenseStatus -}: ProviderProps) { - const [licenseStatusState, setLicenseStatusState] = - useState(licenseStatus); - - const updateLicenseStatus = (updatedLicenseStatus: LicenseStatus) => { - setLicenseStatusState((prev) => { - return { - ...updatedLicenseStatus - }; - }); - }; - - const isUnlocked = () => { - if (licenseStatusState?.isHostLicensed) { - if (licenseStatusState?.isLicenseValid) { - return true; - } - } - return false; - }; - - const isLicenseViolation = () => { - if ( - licenseStatusState?.isHostLicensed && - !licenseStatusState?.isLicenseValid - ) { - return true; - } - if ( - licenseStatusState?.maxSites && - licenseStatusState?.usedSites && - licenseStatusState.usedSites > licenseStatusState.maxSites - ) { - return true; - } - return false; - }; - - return ( - - {children} - - ); -} - -export default LicenseStatusProvider; diff --git a/src/providers/SupporterStatusProvider.tsx b/src/providers/SupporterStatusProvider.tsx deleted file mode 100644 index bcb8be2..0000000 --- a/src/providers/SupporterStatusProvider.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import SupportStatusContext, { - SupporterStatus -} from "@app/contexts/supporterStatusContext"; -import { useState } from "react"; - -interface ProviderProps { - children: React.ReactNode; - supporterStatus: SupporterStatus | null; -} - -export function SupporterStatusProvider({ - children, - supporterStatus -}: ProviderProps) { - const [supporterStatusState, setSupporterStatusState] = - useState(supporterStatus); - - const updateSupporterStatus = ( - updatedSupporterStatus: Partial - ) => { - setSupporterStatusState((prev) => { - if (!prev) { - return updatedSupporterStatus as SupporterStatus; - } - return { - ...prev, - ...updatedSupporterStatus - }; - }); - }; - - return ( - - {children} - - ); -} - -export default SupporterStatusProvider;