diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml index 8022a25bc..3cb010e8c 100644 --- a/.github/workflows/check-be-pull-request.yml +++ b/.github/workflows/check-be-pull-request.yml @@ -24,7 +24,7 @@ jobs: cache: "npm" cache-dependency-path: backend/package-lock.json - name: 📦 Install dependencies - run: npm ci --only-production --ignore-scripts + run: npm ci --only-production working-directory: backend - name: 🧪 Run tests run: npm run test:ci diff --git a/backend/Dockerfile b/backend/Dockerfile index 85b7204fe..e4d283a77 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,7 +4,10 @@ WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --only-production --ignore-scripts +# RUN npm ci --only-production --ignore-scripts +# "prepare": "cd .. && npm install" + +RUN npm ci --only-production COPY . . diff --git a/backend/package-lock.json b/backend/package-lock.json index 85b168c1e..fc03ec328 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", + "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -44,6 +45,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", + "@types/bcrypt": "^5.0.0", + "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", @@ -2576,6 +2579,39 @@ "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@maxmind/geoip2-node": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", @@ -3033,6 +3069,21 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3483,8 +3534,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -3586,7 +3636,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3619,6 +3668,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3803,6 +3869,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -4132,6 +4211,14 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", @@ -4218,6 +4305,14 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4262,6 +4357,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4452,6 +4552,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -4482,6 +4587,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4585,8 +4698,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enabled": { "version": "2.0.0", @@ -4601,6 +4713,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5259,6 +5394,28 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5283,6 +5440,25 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5476,6 +5652,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -5726,7 +5907,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -6497,9 +6677,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -6696,7 +6876,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -6711,7 +6890,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -6880,11 +7058,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -7004,6 +7215,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -9731,6 +9947,17 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10514,6 +10741,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10573,8 +10805,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-swizzle": { "version": "0.2.2", @@ -10795,7 +11026,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10809,7 +11039,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10935,6 +11164,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11403,6 +11648,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", @@ -13768,6 +14021,32 @@ "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + } + } + }, "@maxmind/geoip2-node": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", @@ -14152,6 +14431,21 @@ "@babel/types": "^7.3.0" } }, + "@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -14513,8 +14807,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.8", @@ -14584,8 +14877,7 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", @@ -14606,6 +14898,20 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -14746,6 +15052,15 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + } + }, "before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -14973,6 +15288,11 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, "ci-info": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", @@ -15064,6 +15384,11 @@ "simple-swizzle": "^0.2.2" } }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -15092,6 +15417,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -15237,6 +15567,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -15257,6 +15592,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -15336,8 +15676,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "enabled": { "version": "2.0.0", @@ -15349,6 +15688,28 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -15856,6 +16217,24 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -15873,6 +16252,22 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -16003,6 +16398,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -16181,8 +16581,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -16776,9 +17175,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true }, "jsonwebtoken": { @@ -16944,7 +17343,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "requires": { "semver": "^6.0.0" }, @@ -16952,8 +17350,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -17078,11 +17475,37 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "mmdb-lib": { "version": "2.0.2", @@ -17173,6 +17596,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -19082,6 +19510,17 @@ "path-key": "^3.0.0" } }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -19637,6 +20076,11 @@ "send": "0.18.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -19684,8 +20128,7 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-swizzle": { "version": "0.2.2", @@ -19860,7 +20303,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -19871,7 +20313,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -19960,6 +20401,19 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -20277,6 +20731,14 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", diff --git a/backend/package.json b/backend/package.json index d1a03a74c..0bcb36e49 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", + "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -36,7 +37,6 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { - "prepare": "cd .. && npm install", "start": "npm run build && node build/index.js", "dev": "nodemon", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", @@ -62,6 +62,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", + "@types/bcrypt": "^5.0.0", + "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", diff --git a/backend/src/app.ts b/backend/src/app.ts index 4b02e0311..1d5432f1a 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -38,7 +38,8 @@ import { } from './routes/v1'; import { secret as v2SecretRouter, - workspace as v2WorkspaceRouter + workspace as v2WorkspaceRouter, + serviceTokenData as v2ServiceTokenDataRouter, } from './routes/v2'; import { getLogger } from './utils/logger'; @@ -87,7 +88,7 @@ app.use('/api/v1/membership', v1MembershipRouter); app.use('/api/v1/key', v1KeyRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter); app.use('/api/v1/secret', v1SecretRouter); -app.use('/api/v1/service-token', v1ServiceTokenRouter); +app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate app.use('/api/v1/password', v1PasswordRouter); app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/integration', v1IntegrationRouter); @@ -96,7 +97,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); - +app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next)=>{ diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3fb475099..652b3c7b2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,6 +1,7 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; +const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; @@ -47,6 +48,7 @@ export { PORT, EMAIL_TOKEN_LIFETIME, ENCRYPTION_KEY, + SALT_ROUNDS, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, diff --git a/backend/src/controllers/v1/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts index e21cd7695..244a58783 100644 --- a/backend/src/controllers/v1/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../../config'; * @returns */ export const getServiceToken = async (req: Request, res: Response) => { - // get service token return res.status(200).send({ serviceToken: req.serviceToken }); @@ -73,4 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => { return res.status(200).send({ token }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index bd2ecb15a..d34fecbd7 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -8,6 +8,7 @@ import { IntegrationAuth, IUser, ServiceToken, + ServiceTokenData } from '../../models'; import { createWorkspace as create, @@ -334,4 +335,31 @@ export const getWorkspaceServiceTokens = async ( return res.status(200).send({ serviceTokens }); +} + +export const getWorkspaceServiceTokenData = async ( + req: Request, + res: Response +) => { + let serviceTokenData; + try { + const { workspaceId } = req.query; + + serviceTokenData = await ServiceTokenData + .find({ + workspace: workspaceId + }) + .select('+encryptedKey +iv +tag'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); } \ No newline at end of file diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index dc6977c91..d4729c15c 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,5 +1,7 @@ import * as workspaceController from './workspaceController'; +import * as serviceTokenDataController from './serviceTokenDataController'; export { - workspaceController + workspaceController, + serviceTokenDataController } diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts new file mode 100644 index 000000000..d8f4d4eea --- /dev/null +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -0,0 +1,105 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import { + ServiceTokenData +} from '../../models'; +import { + SALT_ROUNDS +} from '../../config'; + +/** + * Return service token data associated with service token on request + * @param req + * @param res + * @returns + */ +export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).send({ + serviceTokenData: req.serviceTokenData +}); + +/** + * Create new service token data for workspace with id [workspaceId] and + * environment [environment]. + * @param req + * @param res + * @returns + */ +export const createServiceTokenData = async (req: Request, res: Response) => { + let serviceToken, serviceTokenData; + try { + const { + name, + workspaceId, + environment, + encryptedKey, + iv, + tag, + expiresIn + } = req.body; + + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + + serviceTokenData = await new ServiceTokenData({ + name, + workspace: workspaceId, + environment, + user: req.user._id, + expiresAt, + secretHash, + encryptedKey, + iv, + tag + }).save(); + + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); + + if (!serviceTokenData) throw new Error('Failed to find service token data'); + + serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create service token data' + }); + } + + return res.status(200).send({ + serviceToken, + serviceTokenData + }); +} + +/** + * Delete service token data with id [serviceTokenDataId]. + * @param req + * @param res + * @returns + */ +export const deleteServiceTokenData = async (req: Request, res: Response) => { + let serviceTokenData; + try { + const { serviceTokenDataId } = req.params; + + serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 0aafcd525..95c03bbc4 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -24,6 +24,7 @@ import { addMemberships } from '../../helpers/membership'; import { postHogClient, EventService } from '../../services'; import { eventPushSecrets } from '../../events'; import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables'; + interface V2PushSecret { type: string; // personal or shared secretKeyCiphertext: string; @@ -40,326 +41,6 @@ interface V2PushSecret { secretCommentHash?: string; } -/** - * Return public keys of members of workspace with id [workspaceId] - * @param req - * @param res - * @returns - */ -export const getWorkspacePublicKeys = async (req: Request, res: Response) => { - let publicKeys; - try { - const { workspaceId } = req.params; - - publicKeys = ( - await Membership.find({ - workspace: workspaceId - }).populate<{ user: IUser }>('user', 'publicKey') - ) - .filter((m) => m.status === COMPLETED || m.status === GRANTED) - .map((member) => { - return { - publicKey: member.user.publicKey, - userId: member.user._id - }; - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace member public keys' - }); - } - - return res.status(200).send({ - publicKeys - }); -}; - -/** - * Return memberships for workspace with id [workspaceId] - * @param req - * @param res - * @returns - */ -export const getWorkspaceMemberships = async (req: Request, res: Response) => { - let users; - try { - const { workspaceId } = req.params; - - users = await Membership.find({ - workspace: workspaceId - }).populate('user', '+publicKey'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace members' - }); - } - - return res.status(200).send({ - users - }); -}; - -/** - * Return workspaces that user is part of - * @param req - * @param res - * @returns - */ -export const getWorkspaces = async (req: Request, res: Response) => { - let workspaces; - try { - workspaces = ( - await Membership.find({ - user: req.user._id - }).populate('workspace') - ).map((m) => m.workspace); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspaces' - }); - } - - return res.status(200).send({ - workspaces - }); -}; - -/** - * Return workspace with id [workspaceId] - * @param req - * @param res - * @returns - */ -export const getWorkspace = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; - - workspace = await Workspace.findOne({ - _id: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace' - }); - } - - return res.status(200).send({ - workspace - }); -}; - -/** - * Create new workspace named [workspaceName] under organization with id - * [organizationId] and add user as admin - * @param req - * @param res - * @returns - */ -export const createWorkspace = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceName, organizationId } = req.body; - - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: organizationId - }); - - if (!membershipOrg) { - throw new Error('Failed to validate organization membership'); - } - - if (workspaceName.length < 1) { - throw new Error('Workspace names must be at least 1-character long'); - } - - // create workspace and add user as member - workspace = await create({ - name: workspaceName, - organizationId - }); - - await addMemberships({ - userIds: [req.user._id], - workspaceId: workspace._id.toString(), - roles: [ADMIN], - statuses: [GRANTED] - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create workspace' - }); - } - - return res.status(200).send({ - workspace - }); -}; - -/** - * Delete workspace with id [workspaceId] - * @param req - * @param res - * @returns - */ -export const deleteWorkspace = async (req: Request, res: Response) => { - try { - const { workspaceId } = req.params; - - // delete workspace - await deleteWork({ - id: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete workspace' - }); - } - - return res.status(200).send({ - message: 'Successfully deleted workspace' - }); -}; - -/** - * Change name of workspace with id [workspaceId] to [name] - * @param req - * @param res - * @returns - */ -export const changeWorkspaceName = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; - const { name } = req.body; - - workspace = await Workspace.findOneAndUpdate( - { - _id: workspaceId - }, - { - name - }, - { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change workspace name' - }); - } - - return res.status(200).send({ - message: 'Successfully changed workspace name', - workspace - }); -}; - -/** - * Return integrations for workspace with id [workspaceId] - * @param req - * @param res - * @returns - */ -export const getWorkspaceIntegrations = async (req: Request, res: Response) => { - let integrations; - try { - const { workspaceId } = req.params; - - integrations = await Integration.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace integrations' - }); - } - - return res.status(200).send({ - integrations - }); -}; - -/** - * Return (integration) authorizations for workspace with id [workspaceId] - * @param req - * @param res - * @returns - */ -export const getWorkspaceIntegrationAuthorizations = async ( - req: Request, - res: Response -) => { - let authorizations; - try { - const { workspaceId } = req.params; - - authorizations = await IntegrationAuth.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace integration authorizations' - }); - } - - return res.status(200).send({ - authorizations - }); -}; - -/** - * Return service service tokens for workspace [workspaceId] belonging to user - * @param req - * @param res - * @returns - */ -export const getWorkspaceServiceTokens = async ( - req: Request, - res: Response -) => { - let serviceTokens; - try { - const { workspaceId } = req.params; - - serviceTokens = await ServiceToken.find({ - user: req.user._id, - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace service tokens' - }); - } - - return res.status(200).send({ - serviceTokens - }); -} - /** * Upload (encrypted) secrets to workspace with id [workspaceId] * for environment [environment] @@ -434,40 +115,32 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { /** * Return (encrypted) secrets for workspace with id [workspaceId] - * for environment [environment] and (encrypted) workspace key + * for environment [environment] * @param req * @param res * @returns */ export const pullSecrets = async (req: Request, res: Response) => { - // TODO: only return secrets, do not return workspace key - let secrets; - let key; try { const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; - - // validate environment - if (!ENV_SET.has(environment)) { - throw new Error('Failed to validate environment'); + + let userId; + if (req.user) { + userId = req.user._id.toString(); + } else if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id } secrets = await pull({ - userId: req.user._id.toString(), + userId, workspaceId, environment, channel, ipAddress: req.ip }); - - key = await Key.findOne({ - workspace: workspaceId, - receiver: req.user._id - }) - .sort({ createdAt: -1 }) - .populate('sender', '+publicKey'); if (channel !== 'cli') { secrets = reformatPullSecrets({ secrets }); @@ -495,75 +168,30 @@ export const pullSecrets = async (req: Request, res: Response) => { } return res.status(200).send({ - secrets, - key + secrets }); }; -// TODO: modify based on upcoming serviceTokenData changes - -/** - * Return (encrypted) secrets for workspace with id [workspaceId] - * for environment [environment] and (encrypted) workspace key - * via service token - * @param req - * @param res - * @returns - */ - export const pullSecretsServiceToken = async (req: Request, res: Response) => { - let secrets; +export const getWorkspaceKey = async (req: Request, res: Response) => { let key; try { - const environment: string = req.query.environment as string; - const channel: string = req.query.channel as string; const { workspaceId } = req.params; - // validate environment - if (!ENV_SET.has(environment)) { - throw new Error('Failed to validate environment'); - } - - secrets = await pull({ - userId: req.serviceToken.user._id.toString(), - workspaceId, - environment, - channel: 'cli', - ipAddress: req.ip - }); - - key = { - encryptedKey: req.serviceToken.encryptedKey, - nonce: req.serviceToken.nonce, - sender: { - publicKey: req.serviceToken.publicKey - }, - receiver: req.serviceToken.user, - workspace: req.serviceToken.workspace - }; - - if (postHogClient) { - // capture secrets pulled event in production - postHogClient.capture({ - distinctId: req.serviceToken.user.email, - event: 'secrets pulled', - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } + key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }).populate('sender', '+publicKey'); + + if (!key) throw new Error('Failed to find workspace key'); } catch (err) { - Sentry.setUser({ email: req.serviceToken.user.email }); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to pull workspace secrets' + message: 'Failed to get workspace key' }); } return res.status(200).send({ - secrets: reformatPullSecrets({ secrets }), key }); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index cb6897994..c9605a2d0 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -11,7 +11,9 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; router.get( '/:secretId/secret-versions', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 5ae190ccd..bca79dd91 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -11,7 +11,9 @@ import { workspaceController } from '../../controllers/v1'; router.get( '/:workspaceId/secret-snapshots', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 7d5f2461f..ad63d41b4 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,14 +1,140 @@ import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; +import bcrypt from 'bcrypt'; import { - User + User, + ServiceTokenData } from '../models'; import { JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, - JWT_REFRESH_SECRET + JWT_REFRESH_SECRET, + SALT_ROUNDS } from '../config'; +import { + AccountNotFoundError, + ServiceTokenDataNotFoundError, + UnauthorizedRequestError, + BadRequestError +} from '../utils/errors'; + +/** + * Validate that auth token value [authTokenValue] falls under one of + * accepted auth modes [acceptedAuthModes]. + * @param {Object} obj + * @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value) + * @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken) + * @returns {String} authMode - auth mode + */ +const validateAuthMode = ({ + authTokenValue, + acceptedAuthModes +}: { + authTokenValue: string; + acceptedAuthModes: string[]; +}) => { + let authMode; + try { + switch (authTokenValue.split('.', 1)[0]) { + case 'st': + authMode = 'serviceToken'; + break; + default: + authMode = 'jwt'; + break; + } + + if (!acceptedAuthModes.includes(authMode)) + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + + } catch (err) { + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + } + + return authMode; +} + +/** + * Return user payload corresponding to JWT token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - JWT token value + * @returns {User} user - user corresponding to JWT token + */ +const getAuthUserPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let user; + try { + const decodedToken = ( + jwt.verify(authTokenValue, JWT_AUTH_SECRET) + ); + + user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); + + if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); + + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); + + } catch (err) { + throw UnauthorizedRequestError({ + message: 'Failed to authenticate JWT token' + }); + } + + return user; +} + +/** + * Return service token data payload corresponding to service token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - service token value + * @returns {ServiceTokenData} serviceTokenData - service token data + */ +const getAuthSTDPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let serviceTokenData; + try { + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + + // TODO: optimize double query + serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER, 'secretHash expiresAt'); + + if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + // case: service token expired + await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate expired service token' + }); + } + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + + const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); + if (!isMatch) throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); + + serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER) + .select('+encryptedKey +iv +tag'); + + } catch (err) { + throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); + } + + return serviceTokenData; +} /** * Return newly issued (JWT) auth and refresh tokens to user with id [userId] @@ -99,4 +225,11 @@ const createToken = ({ } }; -export { createToken, issueTokens, clearTokens }; +export { + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload, + createToken, + issueTokens, + clearTokens +}; diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index bb2cc875c..119a86abe 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -6,6 +6,7 @@ import requireOrganizationAuth from './requireOrganizationAuth'; import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth'; +import requireServiceTokenDataAuth from './requireServiceTokenDataAuth'; import requireSecretAuth from './requireSecretAuth'; import validateRequest from './validateRequest'; @@ -18,6 +19,7 @@ export { requireIntegrationAuth, requireIntegrationAuthorizationAuth, requireServiceTokenAuth, + requireServiceTokenDataAuth, requireSecretAuth, validateRequest }; diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index d917d362a..8253cb64e 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,8 +1,12 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import { User } from '../models'; -import { JWT_AUTH_SECRET } from '../config'; -import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { User, ServiceTokenData } from '../models'; +import { + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload +} from '../helpers/auth'; +import { BadRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -11,34 +15,53 @@ declare module 'jsonwebtoken' { } /** - * Validate if JWT (auth) token on request is valid (e.g. not expired), - * if there is an associated user, and if that user is fully setup. - * @param req - express request object - * @param res - express response object - * @param next - express next function + * Validate if token on request is valid (e.g. not expired) for various auth modes: + * - If token is a JWT token, then check if there is an associated user + * and if user is fully setup. + * - If token is a service token (st), then check if there is associated + * service token data. + * @param {Object} obj + * @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st) * @returns */ -const requireAuth = async (req: Request, res: Response, next: NextFunction) => { - // JWT authentication middleware - const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] - if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) - if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) - if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) +const requireAuth = ({ + acceptedAuthModes = ['jwt'] +}: { + acceptedAuthModes: string[]; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) + return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') + return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) + return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + + // validate auth token against + const authMode = validateAuthMode({ + authTokenValue: AUTH_TOKEN_VALUE, + acceptedAuthModes + }); + + if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); + + // attach auth payloads + switch (authMode) { + case 'serviceToken': + req.serviceTokenData = await getAuthSTDPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); + break; + default: + req.user = await getAuthUserPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); + break; + } - const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) - ); - - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); - - if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) - if (!user?.publicKey) - return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) - - req.user = user; - return next(); -}; + return next(); + } +} export default requireAuth; diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 94e8363ff..904f4d38e 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -4,6 +4,7 @@ import { ServiceToken } from '../models'; import { JWT_SERVICE_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +// TODO: deprecate declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts new file mode 100644 index 000000000..48eaac3dc --- /dev/null +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -0,0 +1,44 @@ +import { Request, Response, NextFunction } from 'express'; +import { ServiceToken, ServiceTokenData } from '../models'; +import { validateMembership } from '../helpers/membership'; +import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors'; + +type req = 'params' | 'body' | 'query'; + +const requireServiceTokenDataAuth = ({ + acceptedRoles, + acceptedStatuses, + location = 'params' +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; + location?: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const { serviceTokenDataId } = req[location]; + + const serviceTokenData = await ServiceTokenData + .findById(req[location].serviceTokenDataId) + .select('+encryptedKey +iv +tag'); + + if (!serviceTokenData) { + return next(AccountNotFoundError({message: 'Failed to locate service token data'})); + } + + if (req.user) { + // case: jwt auth + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: serviceTokenData.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + } + + req.serviceTokenData = serviceTokenData; + + next(); + } +} + +export default requireServiceTokenDataAuth; \ No newline at end of file diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index e5b8898f3..68edec6a4 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -8,8 +8,8 @@ type req = 'params' | 'body' | 'query'; * Validate if user on request is a member with proper roles for workspace * on request params. * @param {Object} obj - * @param {String[]} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.acceptedStatuses - accepted workspace statuses + * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth + * @param {String[]} obj.acceptedStatuses - accepted workspace statuses for JWT auth * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireWorkspaceAuth = ({ @@ -22,17 +22,28 @@ const requireWorkspaceAuth = ({ location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // workspace authorization middleware - try { - const membership = await validateMembership({ - userId: req.user._id.toString(), - workspaceId: req[location].workspaceId, - acceptedRoles, - acceptedStatuses - }); + const { workspaceId } = req[location]; - req.membership = membership; + if (req.user) { + // case: jwt auth + const membership = await validateMembership({ + userId: req.user._id.toString(), + workspaceId, + acceptedRoles, + acceptedStatuses + }); + + req.membership = membership; + } + + if ( + req.serviceTokenData + && req.serviceTokenData.workspace !== workspaceId + && req.serviceTokenData.environment !== req.query.environment + ) { + next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) + } return next(); } catch (err) { diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 78c38060b..8e934d511 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,6 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; +import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; export { BackupPrivateKey, @@ -47,5 +48,7 @@ export { UserAction, IUserAction, Workspace, - IWorkspace + IWorkspace, + ServiceTokenData, + IServiceTokenData }; diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index 73f705fc8..b5a2f4ec9 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,6 +1,7 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; +// TODO: deprecate export interface IServiceToken { _id: Types.ObjectId; name: string; diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts new file mode 100644 index 000000000..8e8ae5eab --- /dev/null +++ b/backend/src/models/serviceTokenData .ts @@ -0,0 +1,65 @@ +import { Schema, model, Types } from 'mongoose'; +import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; + +export interface IServiceTokenData { + name: string; + workspace: Types.ObjectId; + environment: string; // TODO: adapt to upcoming environment id + user: Types.ObjectId; + expiresAt: Date; + secretHash: string; + encryptedKey: string; + iv: string; + tag: string; +} + +const serviceTokenDataSchema = new Schema( + { + name: { + type: String, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { // TODO: adapt to upcoming environment id + type: String, + required: true + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + expiresAt: { + type: Date + }, + secretHash: { + type: String, + unique: true, + required: true, + select: false + }, + encryptedKey: { + type: String, + select: false + }, + iv: { + type: String, + select: false + }, + tag: { + type: String, + select: false + } + }, + { + timestamps: true + } +); + +const ServiceTokenData = model('ServiceTokenData', serviceTokenDataSchema); + +export default ServiceTokenData; diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index ace15421e..66816788b 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -25,7 +25,19 @@ router.post( authController.login2 ); -router.post('/logout', requireAuth, authController.logout); -router.post('/checkAuth', requireAuth, authController.checkAuth); +router.post( + '/logout', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + authController.logout +); +router.post( + '/checkAuth', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + authController.checkAuth +); export default router; diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index 90a0aa285..0d631b8eb 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -12,7 +12,9 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.get( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -24,7 +26,9 @@ router.get( router.patch( '/:botId/active', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireBotAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index f16aad23b..3715bef46 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -11,7 +11,9 @@ import { integrationController } from '../../controllers/v1'; router.patch( '/:integrationId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -29,7 +31,9 @@ router.patch( router.delete( '/:integrationId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 004706786..c613ce62f 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -12,13 +12,17 @@ import { integrationAuthController } from '../../controllers/v1'; router.get( '/integration-options', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), integrationAuthController.getIntegrationOptions ); router.post( '/oauth-token', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED], @@ -33,7 +37,9 @@ router.post( router.get( '/:integrationAuthId/apps', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -45,7 +51,9 @@ router.get( router.delete( '/:integrationAuthId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED], diff --git a/backend/src/routes/v1/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index 5c89b022f..d8e2d67cc 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -6,7 +6,9 @@ import { membershipOrgController } from '../../controllers/v1'; router.post( '/signup', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('inviteeEmail').exists().trim().notEmpty().isEmail(), body('organizationId').exists().trim().notEmpty(), validateRequest, diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index 094d18e72..d8bc7c29e 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -11,7 +11,9 @@ import { keyController } from '../../controllers/v1'; router.post( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -24,7 +26,9 @@ router.post( router.get( '/:workspaceId/latest', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index 4d81aa6dc..76bf3e57a 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -6,7 +6,9 @@ import { membershipController } from '../../controllers/v1'; router.get( // used for CLI (deprecate) '/:workspaceId/connect', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('workspaceId').exists().trim(), validateRequest, membershipController.validateMembership @@ -14,7 +16,9 @@ router.get( // used for CLI (deprecate) router.delete( '/:membershipId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('membershipId').exists().trim(), validateRequest, membershipController.deleteMembership @@ -22,7 +26,9 @@ router.delete( router.post( '/:membershipId/change-role', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('role').exists().trim(), validateRequest, membershipController.changeMembershipRole diff --git a/backend/src/routes/v1/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts index c74d20449..255b836aa 100644 --- a/backend/src/routes/v1/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -7,7 +7,9 @@ import { membershipOrgController } from '../../controllers/v1'; router.post( // TODO '/membershipOrg/:membershipOrgId/change-role', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('membershipOrgId'), validateRequest, membershipOrgController.changeMembershipOrgRole @@ -15,7 +17,9 @@ router.post( router.delete( '/:membershipOrgId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('membershipOrgId').exists().trim(), validateRequest, membershipOrgController.deleteMembershipOrg diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 1fff9b597..6404c3246 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -11,13 +11,17 @@ import { organizationController } from '../../controllers/v1'; router.get( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), organizationController.getOrganizations ); router.post( // not used on frontend '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('organizationName').exists().trim().notEmpty(), validateRequest, organizationController.createOrganization @@ -25,7 +29,9 @@ router.post( // not used on frontend router.get( '/:organizationId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -37,7 +43,9 @@ router.get( router.get( '/:organizationId/users', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -49,7 +57,9 @@ router.get( router.get( '/:organizationId/my-workspaces', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -61,7 +71,9 @@ router.get( router.patch( '/:organizationId/name', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -74,7 +86,9 @@ router.patch( router.get( '/:organizationId/incidentContactOrg', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -86,7 +100,9 @@ router.get( router.post( '/:organizationId/incidentContactOrg', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -99,7 +115,9 @@ router.post( router.delete( '/:organizationId/incidentContactOrg', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -112,7 +130,9 @@ router.delete( router.post( '/:organizationId/customer-portal-session', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -124,7 +144,9 @@ router.post( router.get( '/:organizationId/subscriptions', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] diff --git a/backend/src/routes/v1/password.ts b/backend/src/routes/v1/password.ts index 2cb811de5..784ef1813 100644 --- a/backend/src/routes/v1/password.ts +++ b/backend/src/routes/v1/password.ts @@ -7,7 +7,9 @@ import { passwordLimiter } from '../../helpers/rateLimiter'; router.post( '/srp1', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('clientPublicKey').exists().trim().notEmpty(), validateRequest, passwordController.srp1 @@ -16,7 +18,9 @@ router.post( router.post( '/change-password', passwordLimiter, - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('clientProof').exists().trim().notEmpty(), body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd body('iv').exists().trim().notEmpty(), // new iv for private key @@ -54,7 +58,9 @@ router.get( router.post( '/backup-private-key', passwordLimiter, - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('clientProof').exists().trim().notEmpty(), body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key body('iv').exists().trim().notEmpty(), // new iv for (backup) private key diff --git a/backend/src/routes/v1/secret.ts b/backend/src/routes/v1/secret.ts index e2dcb3368..c3f0626ce 100644 --- a/backend/src/routes/v1/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -12,7 +12,9 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.post( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -28,7 +30,9 @@ router.post( router.get( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/backend/src/routes/v1/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts index 57031f3e0..edc00a939 100644 --- a/backend/src/routes/v1/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -10,7 +10,7 @@ import { body } from 'express-validator'; import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { serviceTokenController } from '../../controllers/v1'; -// TODO: revoke service token +// note: deprecate service-token routes in favor of service-token data routes/structure router.get( '/', @@ -20,7 +20,9 @@ router.get( router.post( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED], diff --git a/backend/src/routes/v1/user.ts b/backend/src/routes/v1/user.ts index 393978922..e73a6e0f2 100644 --- a/backend/src/routes/v1/user.ts +++ b/backend/src/routes/v1/user.ts @@ -3,6 +3,12 @@ const router = express.Router(); import { requireAuth } from '../../middleware'; import { userController } from '../../controllers/v1'; -router.get('/', requireAuth, userController.getUser); +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + userController.getUser +); export default router; diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index bbd488e06..b14ab3471 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -7,7 +7,9 @@ import { userActionController } from '../../controllers/v1'; // note: [userAction] will be deprecated in /v2 in favor of [action] router.post( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('action'), validateRequest, userActionController.addUserAction @@ -15,7 +17,9 @@ router.post( router.get( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), query('action'), validateRequest, userActionController.getUserAction diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 6a1ae9034..7a1f8c5b8 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -11,7 +11,9 @@ import { workspaceController, membershipController } from '../../controllers/v1' router.get( '/:workspaceId/keys', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -23,7 +25,9 @@ router.get( router.get( '/:workspaceId/users', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -33,11 +37,19 @@ router.get( workspaceController.getWorkspaceMemberships ); -router.get('/', requireAuth, workspaceController.getWorkspaces); +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + workspaceController.getWorkspaces +); router.get( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -49,7 +61,9 @@ router.get( router.post( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('workspaceName').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(), validateRequest, @@ -58,7 +72,9 @@ router.post( router.delete( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], acceptedStatuses: [GRANTED] @@ -70,7 +86,9 @@ router.delete( router.post( '/:workspaceId/name', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -83,7 +101,9 @@ router.post( router.post( '/:workspaceId/invite-signup', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -96,7 +116,9 @@ router.post( router.get( '/:workspaceId/integrations', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -108,7 +130,9 @@ router.get( router.get( '/:workspaceId/authorizations', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -119,8 +143,10 @@ router.get( ); router.get( - '/:workspaceId/service-tokens', - requireAuth, + '/:workspaceId/service-tokens', // deprecate + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -130,4 +156,18 @@ router.get( workspaceController.getWorkspaceServiceTokens ); +router.get( + '/:workspaceId/service-token-data', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokenData +); + export default router; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 6e6758753..acf115a92 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,7 +1,9 @@ import secret from './secret'; import workspace from './workspace'; +import serviceTokenData from './serviceTokenData'; export { secret, - workspace + workspace, + serviceTokenData } diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts new file mode 100644 index 000000000..578d4e38e --- /dev/null +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -0,0 +1,61 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + requireServiceTokenDataAuth, + validateRequest +} from '../../middleware'; +import { param, body } from 'express-validator'; +import { + ADMIN, + MEMBER, + COMPLETED, + GRANTED +} from '../../variables'; +import { serviceTokenDataController } from '../../controllers/v2'; + +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['serviceToken'] + }), + serviceTokenDataController.getServiceTokenData +); + +router.post( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED], + location: 'body' + }), + body('name').exists().trim(), + body('workspaceId'), + body('environment'), + body('encryptedKey'), + body('iv'), + body('tag'), + body('expiresIn'), // measured in ms + validateRequest, + serviceTokenDataController.createServiceTokenData +); + +router.delete( + '/:serviceTokenDataId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireServiceTokenDataAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED], + }), + param('serviceTokenDataId').exists().trim(), + validateRequest, + serviceTokenDataController.deleteServiceTokenData +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 98eacd9ea..b2c91bd1b 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -4,137 +4,16 @@ import { body, param, query } from 'express-validator'; import { requireAuth, requireWorkspaceAuth, - requireServiceTokenAuth, validateRequest } from '../../middleware'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { membershipController } from '../../controllers/v1'; import { workspaceController } from '../../controllers/v2'; -router.get( - '/:workspaceId/keys', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspacePublicKeys -); - -router.get( - '/:workspaceId/users', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspaceMemberships -); - -router.get('/', requireAuth, workspaceController.getWorkspaces); - -router.get( - '/:workspaceId', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspace -); - -router.post( - '/', - requireAuth, - body('workspaceName').exists().trim().notEmpty(), - body('organizationId').exists().trim().notEmpty(), - validateRequest, - workspaceController.createWorkspace -); - -router.delete( - '/:workspaceId', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - acceptedStatuses: [GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.deleteWorkspace -); - -router.post( - '/:workspaceId/name', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('workspaceId').exists().trim(), - body('name').exists().trim().notEmpty(), - validateRequest, - workspaceController.changeWorkspaceName -); - -router.post( - '/:workspaceId/invite-signup', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] - }), - param('workspaceId').exists().trim(), - body('email').exists().trim().notEmpty(), - validateRequest, - membershipController.inviteUserToWorkspace -); - -router.get( - '/:workspaceId/integrations', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspaceIntegrations -); - -router.get( - '/:workspaceId/authorizations', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspaceIntegrationAuthorizations -); - -router.get( // TODO: modify - '/:workspaceId/service-tokens', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspaceServiceTokens -); - router.post( '/:workspaceId/secrets', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -150,7 +29,9 @@ router.post( router.get( '/:workspaceId/secrets', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt', 'serviceToken'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -162,15 +43,18 @@ router.get( workspaceController.pullSecrets ); -router.get( // TODO: modify based on upcoming serviceTokenData changes - '/:workspaceId/secrets-service-token', - requireServiceTokenAuth, - query('environment').exists().trim(), - query('channel'), +router.get( + '/:workspaceId/key', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), param('workspaceId').exists().trim(), - validateRequest, - workspaceController.pullSecretsServiceToken + validateRequest, + workspaceController.getWorkspaceKey ); - export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 33b10e2bc..22f745976 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -16,6 +16,7 @@ declare global { secretSnapshot: any; serviceToken: any; accessToken: any; + serviceTokenData: any; query?: any; } } diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 3c6a385e5..865e27767 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -143,4 +143,14 @@ export const ActionNotFoundError = (error?: Partial) => new stack: error?.stack }); +//* ----->[SERVICE TOKEN DATA ERRORS]<----- +export const ServiceTokenDataNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'service_token_data_not_found_error', + message: error?.message ?? 'The requested service token data was not found', + context: error?.context, + stack: error?.stack +}) + //* ----->[MISC ERRORS]<-----